본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[android] Chrome Custom Tabs

by 돼지왕왕돼지 2019. 1. 26.



chrome custom tabs


-

기존에 특정 web page 를 보여주기 위해서는 external browser 에 의존하거나 internal WebView 를 활용하는 방법만 있었다.

external browser 는 많은 기능을 support 하며 state 를 share 한다는 장점이 있지만, context switch 적 성격이 강하며 customize 하기 어렵다.

internal WebView 의 경우 반대로 context switch 적 성격이 약하고, customize 하기는 쉽지만, state 를 share 하지 못하고, standard 를 맞춰 support 하기 어렵워 많은 공수가 든다는 단점이 있다.


external browser (chrome) 만큼의 호환성을 갖춘 “Custom Tabs” 라는 것이 support library 에 추가되었고, 이것을  internal 처럼 쓸 수 있게 되었다.



-

자신이 제공하는 컨텐츠를 보여줄 때는 WebView 를 쓰는 것도 좋은 방법이다.

그러나 자신이 관리하는 내용이 아닌 외부 link 로 연결하는 경우에는 Chrome Custom Tab 이 추천된다.



-

Chrome custom tabs 를 통해 web page 를 로드하는 것은 기존의 external browser 나 WebView 에 비해 약 2배정도 빠르다.


loading speed chrome vs customtab vs webview


-

Chrome custom tabs 은 다음과 같은 기능을 제공한다. (장점)

    Context switch 의 느낌을 적게 준다. Launch 때도 Activity level 의 화면 전환 느낌이 아니며, 닫을 때도 그렇다.


    Toolbar 영역을 customize 할 수 있다.

        색상을 지정할 수 있다. ( 글자색을 지정할 수 없는 것은 안타깝다. )

        Action Button 1개를 추가할 수 있다.

        Menu Item 을 여러 개 추가할 수 있다.

        Close Icon (Toolbar 최좌측) 을 지정할 수 있다.


    Bottom toolbar 를 customize 할 수 있다.


    Enter, Exit animation 을 지정할 수 있다.


    pre-warming 이라고 부르는 일종의 pre-loading 을 통해 훨씬 빠르게 web page 를 유저에게 보여줄 수 있다.


    Chrome browser 와 cookie 와 permission 을 공유하기 때문에 이미 연결된 사이트(예를 들어 자동 로그인으로 연결된 사이트)에 재연결(재로그인) 할 필요가 없다.


    Activity 에서는 Chrome custom tab 에서 전달하는 Callback 을 받을 수 있다.


    Google 의 Safe Browsing 을 사용하여 이상한 사이트로의 접근을 막아준다.


    (당연하지만) foreground level 로 실행되어 memory kill 에도 대응해준다. 그래서 Data Saver 가 켜져 있어도 이 녀석을 이용하는 데 문제 없다.


    Shared cookie, jar, permission 으로 인해 유연한 브라우징이 가능하다. AutoComplete 연동도 물론!



-

Chrome 45 부터 & JellyBean 부터 사용가능하다.



-

우선 이 녀석을 활용하기 위해서는 dependency 정의가 필요하다.

implementation "androidx.browser:browser:1.2.0"



-

Chrome tab 을 지원하는 package 가 존재하는지 query 가 필요하다

이 때 ACTION_VIEW 도 처리할 수 있으면서 CustomTabService.ACTION_CUSTOM_TABS_CONNECTION 도 처리할 수 있는 녀석이 custom tab 을 지원하는 브라우저 앱이라고 볼 수 있다.

Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, 0);
List<String> packagesSupportingCustomTabs = new ArrayList<>(); 
for (ResolveInfo info : resolvedActivityList) { 
    Intent serviceIntent = new Intent(); 
    serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION); 
    serviceIntent.setPackage(info.activityInfo.packageName); 
    if (pm.resolveService(serviceIntent, 0) != null) { 
        packagesSupportingCustomTabs.add(info.activityInfo.packageName); 
    } 
}


query 후에 지원하는 activity intent 가 여러개일 경우 선별이 필요한데, 참고 글의 필자는 다음과 같은 선별규칙을 거친다.

String packageNameToUser = null
if (packagesSupportingCustomTabs.isEmpty()) {
    packageNameToUser = null; 
} else if (packagesSupportingCustomTabs.size() == 1) { 
    packageNameToUser = packagesSupportingCustomTabs.get(0); 
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) // defaultViewHandlerPackageName 는 pm.resolveActivity 로 query 한 기본 activity
    && !hasSpecializedHandlerIntents(context, activityIntent) // authority 나 path scheme 이 정의되었는지 확인
    && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) { 
    packageNameToUser = defaultViewHandlerPackageName; 
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { // com.android.chrome
    packageNameToUser = STABLE_PACKAGE; 
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { // com.chrome.beta
    packageNameToUser = BETA_PACKAGE; 
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { // com.chrome.dev
    packageNameToUser = DEV_PACKAGE; 
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { // com.google.android.apps.chrome
    packageNameToUser = LOCAL_PACKAGE; 
}



-

Custom tab 을 지원하는 녀석은 bind 할 수 있는 service 를 갖는다.

보통 Activity 의 onStart 에서 bind 를 하고, onStop 에서 unbind 를 한다.

Service 에 연결되서 보통 하는 일은 warmup 이며, 이 때 DNS pre-resolution, pre-connection 등을 한다. 이 과정은 low priority process 에서 진행하여 performance 에 영향을 크게 주지 않는다.

public void bindCustomTabsService(Activity activity) { if (mClient != null) return; // service binding 시 전달되는 binder 역할의 연결체, 이미 연결되었다는 의미 String packageName = CustomTabsHelper.getPackageNameToUse(activity); // 위에서 설명한 custom tab 지원하는 packageName 을 얻어오는 과정 if (packageName == null) return; mConnection = new CustomTabsServiceConnection() { @Override public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { mClient = client; mClient.warmup(0L); // pre-load web page, async, return value 는 async 요청이 수용되었는가 여부 if (mConnectionCallback != null){ mConnectionCallback.onCustomTabsConnected(); } // Initialize a session as soon as possible. getSession(); } @Override public void onServiceDisconnected(ComponentName name) { mClient = null; if (mConnectionCallback != null){ mConnectionCallback.onCustomTabsDisconnected(); } } }; CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection); } public void unbindCustomTabsService(Activity activity) { if (mConnection == null) return; activity.unbindService(mConnection); mClient = null; mCustomTabsSession = null; } public CustomTabsSession getSession() { if (mClient == null) { mCustomTabsSession = null; } else if (mCustomTabsSession == null) { mCustomTabsSession = mClient.newSession(null); // param 은 callback 으로 page load callback 을 받을 수 있다. } return mCustomTabsSession; }


CustomTabSession#mayLaunchUrl(Uri url, Bundle extras, List otherLikelyBundles) 을 통해 chrome 에 유저가 로딩할 페이지에 대한 힌트를 미리 줄 수 있다. 이것이 warmup 을 돕는다. (Bundle 은 future use 를 위한 reserve)

이것을 설정하면 Custom Tab 은 해당 페이지를 pre-fetch 해놓는다. 성능상 이점은 있지만 네트워크나 베터리 코스트의 단점이 있다. (metered network 등의 경우나 저사양 단말 등에서는 pre-rendering 을 하지 않는 등의 고려도 되어 있다.)

참고로 해당 API 는 warmup 이 먼저 실행된 후에 불려야 한다.



-

Service 가 연결이 된 후에는 Custom tab 을 열 수 있다.

UI Customize 는 CustomTabsIntent 를 통해 한다.

private void openCustomTab() {

    // warmup 기능을 제대로 사용하려면, builder 의 constructor 에 CustomTabsSession 을 전달한다.     // CustomTabsIntent 는 supportLib class CustomTabsIntent.Builder intentBuilder = new CustomTabsIntent.Builder(); // titlebar show intentBuilder.setShowTitle(true); // titlebar color - color, 글자색을 바꿀 수 없는 것은 조금 아쉽다 int color = getResources().getColor(R.color.primary); intentBuilder.setToolbarColor(color); // menu items 추가 - label, pendingIntent String menuItemTitle = getString(R.string.menu_title_share); PendingIntent menuItemPendingIntent = createPendingShareIntent(); intentBuilder.addMenuItem(menuItemTitle, menuItemPendingIntent); String menuItemEmailTitle = getString(R.string.menu_title_email); PendingIntent menuItemPendingIntentTwo = createPendingEmailIntent(); intentBuilder.addMenuItem(menuItemEmailTitle, menuItemPendingIntentTwo); // close btn - icon intentBuilder.setCloseButtonIcon(mCloseButtonBitmap); // action btn - icon, label, pendingIntent intentBuilder.setActionButton(mActionButtonBitmap, getString(R.string.menu_title_share), createPendingShareIntent()); // enter & exit animation - 기본은 bottom to top animation 이다 intentBuilder.setStartAnimations(this, R.anim.slide_in_right, R.anim.slide_out_left); intentBuilder.setExitAnimations(this, android.R.anim.slide_in_left, android.R.anim.slide_out_right); // WebViewFallback 은 custom tab 을 지원하는 app 이 없을 때 처리하는 내용을 담고 있다 CustomTabActivityHelper.openCustomTab( this, intentBuilder.build(), Uri.parse(URL_ARGOS), new WebviewFallback()); }

// CustomTabActivityHelper public static void openCustomTab(Activity activity, CustomTabsIntent customTabsIntent, Uri uri, CustomTabFallback fallback) { String packageName = CustomTabsHelper.getPackageNameToUse(activity); //If we cant find a package name, it means there's no browser that supports //Chrome Custom Tabs installed. So, we fallback to the webview if (packageName == null) { if (fallback != null) { fallback.openUri(activity, uri); } } else { customTabsIntent.intent.setPackage(packageName); customTabsIntent.launchUrl(activity, uri); } }



-

Custom Tabs 을 띄울 때는 ACTION_VIEW 를 이용하며, UI customize 를 위해 해당 Intent 의 extras 를 이용한다.

이 말은 Chrome 이 최신 버전이 아닌 경우 시스템 브라우저 또는 유저의 기본 브라우저가 뜰 수 있다는 말. 해당 browser 들이 extra 를 이용해서 기능들을 지원해주길 기대하는 수밖에 없을 듯 하다.



-

현재 설치되어 있는 Chrome 이 Chrome Custom Tabs 를 지원하는지 보려면, bind service 를 해보는 것이 가장 명확하다.

성공하면 지원한다는 것이고, 아니면 지원하지 않는다는 이야기일 가능성이 높다.



-

참고 자료

https://labs.ribot.co.uk/exploring-chrome-customs-tabs-on-android-ef427effe2f4

https://developer.chrome.com/multidevice/android/customtabs

Github Repo : https://github.com/GoogleChrome/custom-tabs-client




끝!





댓글3

  • oneminute 2020.10.08 16:36 신고

    Chrome Custom Tabs를 이용해서 구글 인증(oAuth)같은 것들도 할 수 있을까요?

    기본적으로 웹뷰를 사용하는데..

    구글 로그인 url 일 때를 캐치해서
    구글 인증 부분만 딱 Chrome Custom Tabs를 열어서 진행하고
    로그인이 완료 되었을 때에 Chrome Custom Tabs 를 닫으면서
    로그인 결과를 가져올 수 있는지가 궁금합니다.

    1. 완료되었을 때를 캐치할 수 있는지 ? (특정 url을 캐치한다거나 하는것이 가능한지?)

    2. 사용자의 컨트롤 없이 알아서 Chrome Custom Tabs를 닫을 수 있는지 ?(소스 레벨에서 닫아 줄 수 있는지..)

    위와같은 프로세스를 고집하는건 아닌데..
    가능한지가 궁금하네요

    뜬금 없이 질문해서 죄송합니당..
    답글

    • 안녕하세요!

      제가 직접 실험해본 것이 아니라 정확한 답변을 드리기는 어렵지만, 도움이 되고자 간단한 조사 정보를 공유해드립니다.

      1. https://joebirch.co/android/oauth-on-android-with-custom-tabs/
      위 글을 참조해보면, 구글 OAuth 에서 비슷한 방식을 지원한다면 완료되었을 때를 캐치할 수 있을 것 같습니다.

      2. https://stackoverflow.com/questions/33795847/how-to-close-chrome-custom-tabs
      위 글을 참조해보면 불가능합니다만, 편법으로 Custom Tab 을 띄운 Activity 를 SingleTop 으로 다시 띄움으로써 닫는 효과를 낼 수는 있을 것 같습니다.
      개발자가 제공하는 페이지에서 유저가 특정 액션을 했을 때 닫고 싶다면, 1번의 방법과 2번의 방법을 조합하면 닫을 수 있을 것 같습니다.

      도움이 되셨길 바래요!

  • oneminute 2020.10.14 17:01 신고

    우선 답변 정말 감사드립니다.
    제가 아마 답변해주신 부분을 완벽하게 이해하지는 못했을 수도 있지만

    저희 상황이 웹뷰에서 띄운 페이지가 제가 관여할 수 있는 페이지가 아니고,
    해당 페이지에서 redirect_url 을 설정하여 oAuth 를 진행하고 있어서
    해당 페이지 쪽도 수정이 필요할 것 같기도 하고..

    크롬 커스텀 탭을 닫는 편법 관련해서는
    웹뷰에 띄워져 있는 페이지가 있는데 구글 로그인 완료 시
    화면을 다시 띄우면 부자연스러울 것 같다는 의견도 있고

    여러가지 이유로 다른 방식으로 해결하기로 되었어요

    일반 웹뷰를 사용하는 하이브리드 앱인데
    (저희 페이지도 있지만 하필 문제의 로그인 페이지는 저희 페이지가 아니네요)

    이런 상황에서 구글 로그인 부분만 크롬 탭을 사용하기에는
    조금 억지인 것 같기도 하고..

    결과적으로 많은 도움이 되었습니다.
    소중한 답변 고맙습니다 !
    답글