[카카오톡 링크, 안드로이드 인앱브라우저] 웹에서 카카오톡 링크선택시 안드로이드 WebView의 shouldOverrideUrlLoading API가 호출되지 않습니다

안녕하세요.
현재, 안드로이드로 개발중인 프로젝트가 있습니다.
이 프로젝트는 앱 내부의 WebView에서 Web Url을 Parsing하여 호출하는 로직으로 되어 있습니다.
프로젝트 구조상 Main WebView가 존재하고 필요에 따라 Child WebView를 생성하도록 되어 있습니다.(onCreateWindow)

여기서 문제는 동일한 방식으로 카카오링크를 사용하여 카카오톡 앱을 실행해주는데 Main에서는 shouldOverrideUrlLoading API가 정상적으로 호출되지만 Child WebView에서는 앞선 API가 호출되지 않고 있습니다. (Main 과 Child WebView 모두 Setting이 동일)

웹에서 카카오링크를 호출하는 소스는 다음과 같습니다.

=====================================================

   Kakao.init('id');
    Kakao.Link.sendTalkLink({
    	label: msg,
        image: {
                  	src: imageUrl,
			width: '198',
			height: '198'
        },
        webButton: {
			text: msg,
			url: url
        }
    });

=====================================================

Test Device는 갤럭시S3(4.3), 갤럭시노트4(5.1.1) 2가지로 진행했습니다.
갤럭시S3는 정상동작하며 갤럭시노트4는 호출되지 않고 있습니다.
하지만, 2가지 Device 모두 외부 브라우저에서는 정상동작 합니다.

shouldOverrideUrlLoading API 대신 onPageStarted API가 호출되어 내부 로직을 변경해보았지만,
OS 상하위 버전 모두 사용되어야 하므로 onPageStarted API로 사용하여 구현하는 것은 맞지 않는거 같습니다.

OS Version과 함께 Webkit 변경으로 카카오링크 개발시 추가로 넣어야될 로직이 있는 것인가요 ?
KitKat(19) 이후의 Device들에서 모두 동작하지 않고 있습니다.

확인 부탁드립니다.

안드로이드 4.4 이상부터는 intent 로 앱을 호출합니다.
shouldOverrideUrlLoading 에 intent 에 대한 처리가 되어있나요?

아래글을 참고하셔도 좋을 것 같습니다.
https://devtalk.kakao.com/t/4-4/457/3?u=leon

네. 처리되어 있습니다.

카카오 링크 선택시 onPageStarted 부터 호출되고 shouldOverrideUrlLoading 는 호출되지 않습니다.
shouldOverrideUrlLoading 가 호출되었다면 정상동작 합니다.

아래 글을 보니 궁금한게 있는데요.

kitkat 이상의 환경일때
Main Webview에서는 카카오링크가 동작하고 ChildWebView 에서 동작하지 않는건가요?

네. 맞습니다.
Kitkat 이상일때, onCreateWindow을 통해 Child WebView를 생성하면 shouldOverrideUrlLoading 가 호출되질 않고 있습니다.

그렇다면 이 부분은 실상은 안드로이드 개발에 관련된 부분이라 답해드리기 좀 조심스럽네요.
같은 설정이라고 하셨으니 설정이 다를 것 같지는 않고 child 라서 해서 특별히 더 작업을 해줘 추가해야 되는 부분은 없을 것 같습니다.
다만 kitkat 부터 invalid url에 대해서는 shouldOverrideUrlLoading 이 호출되지 않는데 childWebView 에서만 invalid url 로 인식된다는 게 이상하네요. (안되려면 둘 다 안되어야 할텐데 말이죠)

혹시 이상을 재현해볼 수 있는 샘플 코드 첨부 가능하실까요?

먼저, 확인 감사합니다.

테스트 소스와 테스트를 위한 이미지를 첨부해드립니다.

====================================================================
이미지 삭제

    public class TestActivity extends Activity {

    	// Flag Url Type
    	private final int FLAG_URL_TYPE_NORMAL = 100;
    	private final int FLAG_URL_TYPE_HTTP = 101;
    	private final int FLAG_URL_TYPE_HTTPS = 102;
    	private final int FLAG_URL_TYPE_TEL = 103;
    	private final int FLAG_URL_TYPE_MAIL = 104;
    	private final int FLAG_URL_TYPE_MARKET = 105;
    	private final int FLAG_URL_TYPE_INTENT = 106;
    	// Common
    	private FrameLayout mMainLayout = null;
    	private WebView mMainWeb = null;

    	@Override
    	public void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		setContentView(R.layout.test_main);
    		init();
    	}

    	private void init() {
    		mMainLayout = (FrameLayout) findViewById(R.id.main);
    		initialize();
    	}

    	private void initialize() {
    		loadWebView(URL);
    	}

    	private void loadWebView(String url) {
    		if (mMainWeb == null) {
    			mMainWeb = (WebView) findViewById(R.id.web);
    			mMainWeb.setVerticalScrollbarOverlay(true);
    			mMainWeb.setWebViewClient(new CustomWebViewClient());
    			mMainWeb.setWebChromeClient(new CustomChromeClient());
    			mMainWeb.setOnLongClickListener(new OnLongClickListener() {
    				@Override
    				public boolean onLongClick(View v) {
    					return true;
    				}
    			});
    			mMainWeb.getSettings().setAppCacheEnabled(true);
    			mMainWeb.getSettings().setDomStorageEnabled(true);
    			mMainWeb.getSettings().setSupportMultipleWindows(true);
    			mMainWeb.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
    			mMainWeb.getSettings().setLoadsImagesAutomatically(true);
    			mMainWeb.getSettings().setJavaScriptEnabled(true);
    			mMainWeb.getSettings().setJavaScriptCanOpenWindowsAutomatically(
    					true);
    			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
    				mMainWeb.getSettings().setTextZoom(100);
    			}
    			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    				mMainWeb.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
    			}
    		}
    		mMainWeb.loadUrl(url);
    	}

    	@Override
    	public boolean onKeyDown(int keyCode, KeyEvent event) {
    		if (keyCode == KeyEvent.KEYCODE_BACK) {
    			if (WebUtil.removeChildWebView(mMainLayout, true)) {
    				return true;
    			}
    			if (mMainWeb.canGoBack()) {
    				mMainWeb.goBack();
    				return true;
    			}
    		}
    		return super.onKeyDown(keyCode, event);
    	}

    	// WebChromeClient
    	private class CustomChromeClient extends WebChromeClient {

    		@Override
    		public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture, Message resultMsg) {
    			WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
    			transport.setWebView(createChildWebView());
    			resultMsg.sendToTarget();
    			return true;
    		}

    		private WebView createChildWebView() {
    			WebView childView = new WebView(TestActivity.this);
    			childView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    			childView.setVerticalScrollBarEnabled(false);
    			childView.setHorizontalScrollBarEnabled(false);
    			childView.setWebChromeClient(this);
    			childView.setWebViewClient(new CustomWebViewClient());
    			childView.setOnLongClickListener(new OnLongClickListener() {
    				@Override
    				public boolean onLongClick(View v) {
    					return true;
    				}
    			});
    			childView.getSettings().setAppCacheEnabled(true);
    			childView.getSettings().setDomStorageEnabled(true);
    			childView.getSettings().setSupportMultipleWindows(true);
    			childView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
    			childView.getSettings().setLoadsImagesAutomatically(true);
    			childView.getSettings().setJavaScriptEnabled(true);
    			childView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
    			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
    				childView.getSettings().setTextZoom(100);
    			}
    			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    				childView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
    			}
    			mMainLayout.addView(childView);
    			return childView;
    		}

    		@Override
    		public void onCloseWindow(WebView window) {
    			// TODO Auto-generated method stub
    			super.onCloseWindow(window);
    			window.setVisibility(View.GONE);
    			mMainLayout.removeView(window);
    		}

    		@Override
    		public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    			final JsResult finalRes = result;
    			new AlertDialog.Builder(view.getContext())
    			.setMessage(message)
    			.setCancelable(false)
    			.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
    				@Override
    				public void onClick(DialogInterface dialog, int which) {
    					finalRes.confirm();
    				}
    			}).create().show();
    			return true;
    		}
    	}

    	// WebViewClient
    	private class CustomWebViewClient extends WebViewClient {

    		@Override
    		public boolean shouldOverrideUrlLoading(WebView view, String url) {
    			showWebPage(view, url);
    			return true;
    		}

    		@Override
    		public void onPageStarted(WebView view, String url, Bitmap favicon) {
    			super.onPageStarted(view, url, favicon);
    		}

    		@Override
    		public void onPageFinished(WebView view, String url) {
    			super.onPageFinished(view, url);
    		}

    		@Override
    		public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
    			super.onReceivedError(view, errorCode, description, failingUrl);
    		}
    	}

    	private void showWebPage(WebView view, final String url) {
    		switch (getUrlType(url)) {
    		case FLAG_URL_TYPE_NORMAL:
    			Uri uri = null;
    			try {
    				uri = Uri.parse(url);
    				startActivityForUrl(url);
    			} catch (Exception e) {
    				if (uri == null) {
    					// Toast
    				} else {
    					startActivityForUrl(getMarketUrlByScheme(uri.getScheme(), null));
    				}
    			}
    			break;

    		case FLAG_URL_TYPE_HTTP:
    		case FLAG_URL_TYPE_HTTPS:
    			view.loadUrl(url);
    			break;

    		case FLAG_URL_TYPE_TEL:
    			// Dialog
    			break;

    		case FLAG_URL_TYPE_MAIL:
    			startActivityForChooser(url, Intent.ACTION_SENDTO);
    			break;

    		case FLAG_URL_TYPE_MARKET:
    		case FLAG_URL_TYPE_INTENT:
    			Intent intent = null;
    			try {
    				intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
    				startActivityForIntent(intent);
    			} catch (Exception e) {
    				if (intent == null) {
    					// Toast
    				} else {
    					startActivityForUrl(getMarketUrlByScheme(intent.getScheme(), intent.getPackage()));
    				}
    			}
    			break;
    		}
    	}

    	private int getUrlType(String url) {
    		try {
    			Uri uri = Uri.parse(url);
    			if (uri.getScheme().equalsIgnoreCase("http")) {
    				return FLAG_URL_TYPE_HTTP;
    			} else if (uri.getScheme().equalsIgnoreCase("https")) {
    				return FLAG_URL_TYPE_HTTPS;
    			} else if (uri.getScheme().equalsIgnoreCase("tel")) {
    				return FLAG_URL_TYPE_TEL;
    			} else if (uri.getScheme().equalsIgnoreCase("mailto")) {
    				return FLAG_URL_TYPE_MAIL;
    			} else if (uri.getScheme().equalsIgnoreCase("market")) {
    				return FLAG_URL_TYPE_MARKET;
    			} else if (uri.getScheme().equalsIgnoreCase("intent")) {
    				return FLAG_URL_TYPE_INTENT;
    			} else {
    				return FLAG_URL_TYPE_NORMAL;
    			}
    		} catch (Exception e) {
    			return 0;
    		}
    	}

    	private String getMarketUrlByScheme(String scheme, String packageName) {
    		if (!CommonUtil.checkStrValidate(packageName)) {
    			return "market://details?id=" + packageName;
    		}
    		if (!CommonUtil.checkStrValidate(scheme)) {
    			if (scheme.equalsIgnoreCase("kakaolink")) { // KakaoTalk
    				return "market://details?id=" + "com.kakao.talk";
    			} else if (scheme.equalsIgnoreCase("storylink")) { // KakaoStory
    				return "market://details?id=" + "com.kakao.story";
    			}
    		}
    		return null;
    	}

    	private void startActivityForIntent(Intent intent) {
    		if (intent != null) {
    			startActivity(intent);
    		}
    	}

    	private void startActivityForUrl(String url) {
    		startActivityForIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
    	}

    	private void startActivityForChooser(String url, String flag) {
    		startActivityForIntent(Intent.createChooser(new Intent(flag, Uri.parse(url)), getString(R.string.dlg_choice)));
    	}
    }

====================================================================

풀소스와 발생경로 이미지를 첨부해드렸습니다.

그럼, Web 쪽에서 호출하는 KakaoLink Value들에 문제가 있는 것인가요 ?

확인 부탁드리겠습니다.

해당 이슈는 KakaoLink 와 관계없는 것 같구요. 안드로이드 구현상의 이슈인 것 같습니다.
공식적으로 kakao JS 는 WebView와 하이브리드앱을 지원하지 않습니다만 개발자 편의를 위해 우회할 수 있는 가이드를 비공식적으로 제공해드리고 있습니다.(shouldOverrideUrlLoading 에서 custom scheme 처리하는 방식)
알려주신 케이스처럼 팝업을 처리했을때는 shouldOverrideUrlLoading가 호출되고 있지 않은데요.
이것은 url 의 유효성과는 상관없이 발생하는 것 같네요.
이에 대해서는 좀 더 확인해봐야겠지만 해결을 장담하기는 어렵겠습니다. 좀 더 확인은 해볼게요~

1개의 좋아요

네. 확인 감사합니다!

Android Chrome WebView 의 이슈인 것 같네요. multiwindow 로 url 을 열었을 시(즉 child webview로 열었을 때) shouldOverrideUrlLoading 호출에 버그가 있는 것 같습니다.
https://bugs.chromium.org/p/chromium/issues/detail?id=611098

쓰레드의 내용으로 볼때 버그가 수정이 될 것 같지는 않고 멀티윈도우를 사용하지 않거나 다른 방식으로 우회가 필요할 것 같습니다.

1개의 좋아요