Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Chrome Custom Tabsの仕組みから学ぶプロセス間通信

Chrome Custom Tabsの仕組みから学ぶプロセス間通信

Chrome Custom Tabsの仕組みから学ぶプロセス間通信
Room 1 - 2019/02/07 14:50-15:20
の資料です。

Chrome Custom Tabsを使うと簡単にそのアプリの一機能であるかのようにブラウザを呼び出すことができます。この仕組みはChromeだけの機能ではなくFirefoxなど多くのブラウザアプリで実装されています。

このセッションではChrome Custom Tabsで呼び出すことのできるブラウザアプリを作るというアプローチで、Chrome Custom Tabsの仕組みを通じ、アプリ同士を連携させるプロセス間通信の使い方についてじっくりと解説します。

Avatar for 大前良介 (OHMAE Ryosuke)

大前良介 (OHMAE Ryosuke)

February 07, 2019
Tweet

More Decks by 大前良介 (OHMAE Ryosuke)

Other Decks in Technology

Transcript

  1. Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent,

    0); for (ResolveInfo info : resolvedActivityList) { Intent serviceIntent = new Intent(); serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); serviceIntent.setPackage(info.activityInfo.packageName); if (pm.resolveService(serviceIntent, 0) != null) { packagesSupportingCustomTabs .add(info.activityInfo.packageName); } } 適当なURLを起動するIntent
  2. Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent,

    0); for (ResolveInfo info : resolvedActivityList) { Intent serviceIntent = new Intent(); serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); serviceIntent.setPackage(info.activityInfo.packageName); if (pm.resolveService(serviceIntent, 0) != null) { packagesSupportingCustomTabs .add(info.activityInfo.packageName); } } ブラウザアプリのリスト
  3. Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent,

    0); for (ResolveInfo info : resolvedActivityList) { Intent serviceIntent = new Intent(); serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); serviceIntent.setPackage(info.activityInfo.packageName); if (pm.resolveService(serviceIntent, 0) != null) { packagesSupportingCustomTabs .add(info.activityInfo.packageName); } } android.support.customtabs.action .CustomTabsService Actionを受け取る Intent-Filterを持つサービス
  4. Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent,

    0); for (ResolveInfo info : resolvedActivityList) { Intent serviceIntent = new Intent(); serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION); serviceIntent.setPackage(info.activityInfo.packageName); if (pm.resolveService(serviceIntent, 0) != null) { packagesSupportingCustomTabs .add(info.activityInfo.packageName); } }
  5. class CustomTabsHelper() : CustomTabsServiceConnection() { override fun onCustomTabsServiceConnected( name: ComponentName,

    client: CustomTabsClient) { client.warmup(0) val session = client.newSession(CustomTabsCallback()) session.mayLaunchUrl(Uri.parse(url), null, urlList) } override fun onServiceDisconnected(name: ComponentName) {} }
  6. interface ICustomTabsService { boolean warmup(long flags) = 1; boolean newSession(in

    ICustomTabsCallback callback) = 2; boolean mayLaunchUrl(in ICustomTabsCallback callback, in Uri url, in Bundle extras, in List<Bundle> otherLikelyBundles) = 3; Bundle extraCommand(String commandName, in Bundle args) = 4; boolean updateVisuals(in ICustomTabsCallback callback, in Bundle bundle) = 5; boolean requestPostMessageChannel(in ICustomTabsCallback callback, in Uri postMessageOrigin) = 6; int postMessage(in ICustomTabsCallback callback, String message, in Bundle extras) = 7; boolean validateRelationship(in ICustomTabsCallback callback, int relation, in Uri origin, in Bundle extras) = 8; } AIDL
  7. class CustomTabsConnectionService : CustomTabsService() { override fun warmup(flags: Long): Boolean

    = false override fun newSession(sessionToken: CustomTabsSessionToken?): Boolean = true override fun mayLaunchUrl( sessionToken: CustomTabsSessionToken?, url: Uri?, extras: Bundle?, otherLikelyBundles: MutableList<Bundle>?): Boolean = true override fun extraCommand(commandName: String?, args: Bundle?): Bundle? = null override fun requestPostMessageChannel( sessionToken: CustomTabsSessionToken?, postMessageOrigin: Uri?): Boolean = false override fun postMessage( sessionToken: CustomTabsSessionToken?, message: String?, extras: Bundle? ): Int = CustomTabsService.RESULT_FAILURE_DISALLOWED override fun validateRelationship( sessionToken: CustomTabsSessionToken?, relation: Int, origin: Uri?, extras: Bundle?): Boolean = false override fun updateVisuals( sessionToken: CustomTabsSessionToken?, bundle: Bundle?): Boolean = false } 起動先
  8. 起動先 class MainService : Service() { override fun onBind(intent: Intent):

    IBinder { return object : IRemoteService.Stub() { override fun sendMessage(message: String): String { return message } } } }
  9. val intent = Intent(Const.ACTION_AIDL) intent.setPackage("net.mm2d.aidl.app1") bindService(intent, object : ServiceConnection {

    override fun onServiceConnected( name: ComponentName?, service: IBinder?) { remoteService = IRemoteService.Stub.asInterface(service) remoteService.sendMessage("message") } override fun onServiceDisconnected(name: ComponentName?) { remoteService = null } }, Context.BIND_AUTO_CREATE) 別プロセスの メソッドをコール
  10. public Builder setToolbarColor(@ColorInt int color) { mIntent.putExtra(EXTRA_TOOLBAR_COLOR, color); return this;

    } val toolbarColor: Int = intent.getIntExtra(EXTRA_TOOLBAR_COLOR, Color.WHITE) 起動元 起動先
  11. public Builder setToolbarColor(@ColorInt int color) { mIntent.putExtra(EXTRA_TOOLBAR_COLOR, color); return this;

    } val toolbarColor: Int = intent.getIntExtra(EXTRA_TOOLBAR_COLOR, Color.WHITE) 起動元 起動先
  12. public Builder setCloseButtonIcon(@NonNull Bitmap icon) { mIntent.putExtra(EXTRA_CLOSE_BUTTON_ICON, icon); return this;

    } val closeIcon: Bitmap? = intent.getParcelableExtra(EXTRA_CLOSE_BUTTON_ICON) 起動元 起動先
  13. public Builder setCloseButtonIcon(@NonNull Bitmap icon) { mIntent.putExtra(EXTRA_CLOSE_BUTTON_ICON, icon); return this;

    } val closeIcon: Bitmap? = intent.getParcelableExtra(EXTRA_CLOSE_BUTTON_ICON) 起動元 起動先
  14. public Builder(@Nullable CustomTabsSession session) { if (session != null) mIntent.setPackage(session.getComponentName()

    .getPackageName()); Bundle bundle = new Bundle(); BundleCompat.putBinder( bundle, EXTRA_SESSION, session == null ? null : session.getBinder()); mIntent.putExtras(bundle); }
  15. public Builder(@Nullable CustomTabsSession session) { if (session != null) mIntent.setPackage(session.getComponentName()

    .getPackageName()); Bundle bundle = new Bundle(); BundleCompat.putBinder( bundle, EXTRA_SESSION, session == null ? null : session.getBinder()); mIntent.putExtras(bundle); }
  16. public final class CustomTabsSession { ... /* package */ IBinder

    getBinder() { return mCallback.asBinder(); }
  17. interface ICallback1 { void callback(String message); } val binder =

    BundleCompat.getBinder( intent.extras, Const.EXTRA_CALLBACK1) ICallback1.Stub.asInterface(binder).callback("message") val callback = object : ICallback1.Stub() { override fun callback(message: String?) {...} } intent.putExtras(Bundle().also { BundleCompat.putBinder(it, Const.EXTRA_CALLBACK1, callback) }) startActivity(intent) AIDL 起動元 起動先
  18. interface ICallback2 { Bitmap callback(); } val binder = BundleCompat.getBinder(

    intent.extras, Const.EXTRA_CALLBACK2) val bitmap = ICallback2.Stub.asInterface(binder).callback() val callback = object : ICallback2.Stub() { override fun callback(): Bitmap? {...} } intent.putExtras(Bundle().also { BundleCompat.putBinder(it, Const.EXTRA_CALLBACK2, callback) }) startActivity(intent) AIDL 起動元 起動先 1MB以上 OK
  19. public static Intent setAlwaysUseBrowserUI(Intent intent) { if (intent == null)

    intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(EXTRA_USER_OPT_OUT_FROM_CUSTOM_TABS, true); return intent; } public static boolean shouldAlwaysUseBrowserUI(Intent intent) { return intent.getBooleanExtra( EXTRA_USER_OPT_OUT_FROM_CUSTOM_TABS, false) && (intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0; } 起動元 起動先
  20. public static Intent setAlwaysUseBrowserUI(Intent intent) { if (intent == null)

    intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(EXTRA_USER_OPT_OUT_FROM_CUSTOM_TABS, true); return intent; } public static boolean shouldAlwaysUseBrowserUI(Intent intent) { return intent.getBooleanExtra( EXTRA_USER_OPT_OUT_FROM_CUSTOM_TABS, false) && (intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0; } 起動元 起動先
  21. <activity android:name=".IntentDispatcher"> <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data

    android:scheme="http"/> <data android:scheme="https"/> </intent-filter> </activity> <activity android:name=".BrowserActivity"/> <activity android:name=".CustomTabsActivity"/> 踏み台 Activity
  22. class IntentDispatcher : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) val launchIntent = intent ?: return if (isCustomTabIntent(launchIntent)) { launchIntent.setClass(this, CustomTabsActivity::class.java) } else { launchIntent.setClass(this, BrowserActivity::class.java) } startActivity(launchIntent) finish() } private fun isCustomTabIntent(intent: Intent): Boolean { if (CustomTabsIntent.shouldAlwaysUseBrowserUI(intent)) return false if (!intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)) return false return intent.data != null }
  23. class IntentDispatcher : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) val launchIntent = intent ?: return if (isCustomTabIntent(launchIntent)) { launchIntent.setClass(this, CustomTabsActivity::class.java) } else { launchIntent.setClass(this, BrowserActivity::class.java) } startActivity(launchIntent) finish() } private fun isCustomTabIntent(intent: Intent): Boolean { if (CustomTabsIntent.shouldAlwaysUseBrowserUI(intent)) return false if (!intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)) return false return intent.data != null }
  24. class IntentDispatcher : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState) val launchIntent = intent ?: return if (isCustomTabIntent(launchIntent)) { launchIntent.setClass(this, CustomTabsActivity::class.java) } else { launchIntent.setClass(this, BrowserActivity::class.java) } startActivity(launchIntent) finish() } private fun isCustomTabIntent(intent: Intent): Boolean { if (CustomTabsIntent.shouldAlwaysUseBrowserUI(intent)) return false if (!intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)) return false return intent.data != null }
  25. public Builder setStartAnimations( @NonNull Context context, @AnimRes int enterResId, @AnimRes

    int exitResId) { mStartAnimationBundle = ActivityOptionsCompat.makeCustomAnimation( context, enterResId, exitResId).toBundle(); return this; } public void launchUrl(Context context, Uri url) { intent.setData(url); ContextCompat.startActivity(context, intent, startAnimationBundle); }
  26. public Builder setExitAnimations( @NonNull Context context, @AnimRes int enterResId, @AnimRes

    int exitResId) { Bundle bundle = ActivityOptionsCompat.makeCustomAnimation( context, enterResId, exitResId).toBundle(); mIntent.putExtra(EXTRA_EXIT_ANIMATION_BUNDLE, bundle); return this; } b.putInt(KEY_ANIM_ENTER_RES_ID, mCustomEnterResId); b.putInt(KEY_ANIM_EXIT_RES_ID, mCustomExitResId);
  27. override fun finish() { super.finish() overridePackageName = true overridePendingTransition( reader.enterAnimationRes,

    reader.exitAnimationRes) overridePackageName = false } override fun getPackageName(): String { if (overridePackageName) return reader.clientPackageName ?: super.getPackageName() return super.getPackageName() } 起動先
  28. public Builder setSecondaryToolbarViews( @NonNull RemoteViews remoteViews, @Nullable int[] clickableIDs, @Nullable

    PendingIntent pendingIntent) { mIntent.putExtra(EXTRA_REMOTEVIEWS, remoteViews); mIntent.putExtra(EXTRA_REMOTEVIEWS_VIEW_IDS, clickableIDs); mIntent.putExtra(EXTRA_REMOTEVIEWS_PENDINGINTENT, pendingIntent); return this; }
  29. public Builder setSecondaryToolbarViews( @NonNull RemoteViews remoteViews, @Nullable int[] clickableIDs, @Nullable

    PendingIntent pendingIntent) { mIntent.putExtra(EXTRA_REMOTEVIEWS, remoteViews); mIntent.putExtra(EXTRA_REMOTEVIEWS_VIEW_IDS, clickableIDs); mIntent.putExtra(EXTRA_REMOTEVIEWS_PENDINGINTENT, pendingIntent); return this; }
  30. val remoteViews: RemoteViews? = intent.getParcelableExtra(EXTRA_REMOTEVIEWS) val remoteViewsClickableIDs: IntArray? = intent.getIntArrayExtra(EXTRA_REMOTEVIEWS_VIEW_IDS)

    val remoteViewsPendingIntent: PendingIntent? = intent.getParcelableExtra(EXTRA_REMOTEVIEWS_PENDINGINTENT) 起動先
  31. public Builder addMenuItem( @NonNull String label, @NonNull PendingIntent pendingIntent) {

    if (mMenuItems == null) mMenuItems = new ArrayList<>(); Bundle bundle = new Bundle(); bundle.putString(KEY_MENU_ITEM_TITLE, label); bundle.putParcelable(KEY_PENDING_INTENT, pendingIntent); mMenuItems.add(bundle); return this; } public CustomTabsIntent build() { if (mMenuItems != null) { mIntent.putParcelableArrayListExtra( CustomTabsIntent.EXTRA_MENU_ITEMS, mMenuItems); } ... }
  32. public Builder addMenuItem( @NonNull String label, @NonNull PendingIntent pendingIntent) {

    if (mMenuItems == null) mMenuItems = new ArrayList<>(); Bundle bundle = new Bundle(); bundle.putString(KEY_MENU_ITEM_TITLE, label); bundle.putParcelable(KEY_PENDING_INTENT, pendingIntent); mMenuItems.add(bundle); return this; } public CustomTabsIntent build() { if (mMenuItems != null) { mIntent.putParcelableArrayListExtra( CustomTabsIntent.EXTRA_MENU_ITEMS, mMenuItems); } ... }
  33. public Builder addMenuItem( @NonNull String label, @NonNull PendingIntent pendingIntent) {

    if (mMenuItems == null) mMenuItems = new ArrayList<>(); Bundle bundle = new Bundle(); bundle.putString(KEY_MENU_ITEM_TITLE, label); bundle.putParcelable(KEY_PENDING_INTENT, pendingIntent); mMenuItems.add(bundle); return this; } public CustomTabsIntent build() { if (mMenuItems != null) { mIntent.putParcelableArrayListExtra( CustomTabsIntent.EXTRA_MENU_ITEMS, mMenuItems); } ... }
  34. fun makeMenuParamsList(intent: Intent): List<MenuParams> { return intent .getParcelableArrayListExtra<Bundle>(EXTRA_MENU_ITEMS) ?.map {

    makeMenuParams(it) } ?: emptyList() } fun makeMenuParams(bundle: Bundle): MenuParams? { return MenuParams( bundle.getString(KEY_MENU_ITEM_TITLE), bundle.getParcelable(KEY_PENDING_INTENT)) } 起動先
  35. fun makeMenuParamsList(intent: Intent): List<MenuParams> { return intent .getParcelableArrayListExtra<Bundle>(EXTRA_MENU_ITEMS) ?.map {

    makeMenuParams(it) } ?: emptyList() } fun makeMenuParams(bundle: Bundle): MenuParams? { return MenuParams( bundle.getString(KEY_MENU_ITEM_TITLE), bundle.getParcelable(KEY_PENDING_INTENT)) } 起動先
  36. fun makeMenuParamsList(intent: Intent): List<MenuParams> { return intent .getParcelableArrayListExtra<Bundle>(EXTRA_MENU_ITEMS) ?.map {

    makeMenuParams(it) } ?: emptyList() } fun makeMenuParams(bundle: Bundle): MenuParams? { return MenuParams( bundle.getString(KEY_MENU_ITEM_TITLE), bundle.getParcelable(KEY_PENDING_INTENT)) } 起動先
  37. fun onSelectCustomMenu(index: Int) { reader.menuParamsList[index] .pendingIntent?.send() } fun sendPendingIntentWithUrl(pendingIntent: PendingIntent)

    { val addedIntent = Intent().also { it.data = Uri.parse(web_view.url) } pendingIntent.send(this, 0, addedIntent) }
  38. fun onSelectCustomMenu(index: Int) { reader.menuParamsList[index] .pendingIntent?.send() } fun sendPendingIntentWithUrl(pendingIntent: PendingIntent)

    { val addedIntent = Intent().also { it.data = Uri.parse(web_view.url) } pendingIntent.send(this, 0, addedIntent) }
  39. fun onSelectCustomMenu(index: Int) { reader.menuParamsList[index] .pendingIntent?.send() } fun sendPendingIntentWithUrl(pendingIntent: PendingIntent)

    { val addedIntent = Intent().also { it.data = Uri.parse(web_view.url) } pendingIntent.send(this, 0, addedIntent) }