Slide 1

Slide 1 text

Chrome Custom Tabs の仕組みから学ぶ プロセス間通信 大前良介(OHMAE Ryosuke) DroidKaigi 2019

Slide 2

Slide 2 text

自己紹介 大前良介(OHMAE Ryosuke) https://github.com/ohmae ヤフー株式会社 Androidアプリエンジニア Yahoo! JAPAN Yahoo!ブラウザー buzzHOME

Slide 3

Slide 3 text

Chrome Custom Tabs

Slide 4

Slide 4 text

Chrome Custom Tabs 使ったことありますか? 質問

Slide 5

Slide 5 text

Chrome Custom Tabs アプリ内ブラウザのような見た目で Chromeを呼び出すことができる

Slide 6

Slide 6 text

使い方は?

Slide 7

Slide 7 text

ライブラリ •SupportLibrary com.android.support:customtabs •Jetpack androidx.browser:browser

Slide 8

Slide 8 text

CustomTabsClient.bindCustomTabsService( context, packageName, this) ... CustomTabsIntent.Builder(session) ... .build() .launchUrl(this, Uri.parse(url)) バインドしてセッション指定 com.android.support:customtabs

Slide 9

Slide 9 text

val customTabsIntent = CustomTabsIntent.Builder() ... .build() customTabsIntent.intent.setPackage(packageName) customTabsIntent.launchUrl(this, Uri.parse(url)) Intentにパッケージ指定

Slide 10

Slide 10 text

利用するパッケージの決定方法 https://github.com/ GoogleChrome/custom-tabs-client /shared/src/main/java/org/chromium/customtabsclient/shared/ CustomTabsHelper.java String getPackageNameToUse(Context)

Slide 11

Slide 11 text

Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); List 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

Slide 12

Slide 12 text

Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); List 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); } } ブラウザアプリのリスト

Slide 13

Slide 13 text

Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); List 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を持つサービス

Slide 14

Slide 14 text

使用されるアプリは ブラウザ かつ Custom Tabsサービス を持つアプリ

Slide 15

Slide 15 text

Chromeだけが 選ばれるわけじゃない

Slide 16

Slide 16 text

Firefox Custom Tabs

Slide 17

Slide 17 text

他にもいっぱい対応ブラウザ https://github.com/ohmae/ custom-tabs-sample

Slide 18

Slide 18 text

・・・ってことは アプリを限定しない 汎用的なプロトコル!?

Slide 19

Slide 19 text

CustomTabs対応ブラウザ 作ってみました https://github.com/ohmae/custom-tabs-browser

Slide 20

Slide 20 text

Android的正攻法な プロセス間通信の ノウハウの宝庫

Slide 21

Slide 21 text

本日の内容 Custom Tabsで使われている Android的正攻法な プロセス間通信を紹介 基本中の基本だけど奥深い!

Slide 22

Slide 22 text

Androidのプロセス間通信 •Intent • startActivity/startActivityForResult • startService/sendBroadcast •AIDL(Android Interface Definition Language) •Content Provider •その他(Socket通信・ファイル共有・etc.)

Slide 23

Slide 23 text

Custom Tabsで使われるプロセス間通信 •Intent • startActivity/startActivityForResult • startService/sendBroadcast •AIDL(Android Interface Definition Language) •Content Provider •その他(Socket通信・ファイル共有・etc.)

Slide 24

Slide 24 text

プロトコルに対応した アプリを探す

Slide 25

Slide 25 text

Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")); List 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); } }

Slide 26

Slide 26 text

PackageManager #queryIntentActivities #queryIntentServices #resolveActivity #resolveService 等をつかって探す

Slide 27

Slide 27 text

AndroidManifest

Slide 28

Slide 28 text

Slide 29

Slide 29 text

Slide 30

Slide 30 text

プロトコルの宣言は intent-filter AndroidManifestは アプリの仕様宣言

Slide 31

Slide 31 text

AIDL

Slide 32

Slide 32 text

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) {} }

Slide 33

Slide 33 text

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 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

Slide 34

Slide 34 text

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?): 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 } 起動先

Slide 35

Slide 35 text

複雑なので簡単な例で

Slide 36

Slide 36 text

AIDL interface IRemoteService { String sendMessage(String message); }

Slide 37

Slide 37 text

起動先 class MainService : Service() { override fun onBind(intent: Intent): IBinder { return object : IRemoteService.Stub() { override fun sendMessage(message: String): String { return message } } } }

Slide 38

Slide 38 text

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) 別プロセスの メソッドをコール

Slide 39

Slide 39 text

AIDL - メリット •アプリ間でメソッドコールが可能 •引数・戻り値ともに使える •データ型も柔軟 •プリミティブ型・String・Parcelable •それらを要素とするList・Map •比較的大きなデータも送受信可

Slide 40

Slide 40 text

AIDL - 注意点 •バインド先プロセスが生きている必要あり •DeadObjectException •スレッドセーフに作る •アプリをまたいだメソッドコールはBinderス レッドで実行される •密結合にならないようによく考えて •一度リリースしてしまうと修正ほぼ不可

Slide 41

Slide 41 text

Intent

Slide 42

Slide 42 text

CustomTabsIntent ってどんなIntent?

Slide 43

Slide 43 text

Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtras(bundle); intent.putExtra(...) intent.setData(uri); context.startActivity(intent, startAnimationBundle);

Slide 44

Slide 44 text

URIを開くIntent + Extra

Slide 45

Slide 45 text

Intent経由で渡せるパラメータ •Action(String) •Category(String) •Data(Uri) •Extra(Bundle)

Slide 46

Slide 46 text

Intent経由で渡せるパラメータ •Action(String) •Category(String) •Data(Uri) •Extra(Bundle)

Slide 47

Slide 47 text

ツールバー色・クローズアイコン

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

プリミティブ型 Parcelable そのまま使える

Slide 53

Slide 53 text

TransactionTooLargeException 大きなデータを渡すことはできない 注意

Slide 54

Slide 54 text

コールバック

Slide 55

Slide 55 text

CustomTabsIntent.Builder(session) ... .build() .launchUrl(this, Uri.parse(url))

Slide 56

Slide 56 text

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); }

Slide 57

Slide 57 text

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); }

Slide 58

Slide 58 text

public final class CustomTabsSession { ... /* package */ IBinder getBinder() { return mCallback.asBinder(); }

Slide 59

Slide 59 text

IBinderをExtraで 渡すことができる

Slide 60

Slide 60 text

val callback: CustomTabsCallback? = try { CustomTabsSessionToken .getSessionTokenFromIntent(intent)?.callback } catch (e: Exception) { null }

Slide 61

Slide 61 text

単純な例

Slide 62

Slide 62 text

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 起動元 起動先

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

ブラウザモード CustomTabsモード Activityを分ける

Slide 65

Slide 65 text

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; } 起動元 起動先

Slide 66

Slide 66 text

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; } 起動元 起動先

Slide 67

Slide 67 text

Intent-filterには Extraの条件をつけられない

Slide 68

Slide 68 text

踏み台 Activity

Slide 69

Slide 69 text

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 }

Slide 70

Slide 70 text

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 }

Slide 71

Slide 71 text

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 }

Slide 72

Slide 72 text

アニメーション

Slide 73

Slide 73 text

CustomTabsIntent.Builder(session) .setStartAnimations(this, R.anim.slide_in_right, R.anim.slide_out_left) .setExitAnimations(this, R.anim.slide_in_left, R.anim.slide_out_right) .build() .launchUrl(this, Uri.parse(url))

Slide 74

Slide 74 text

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); }

Slide 75

Slide 75 text

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);

Slide 76

Slide 76 text

リソースID

Slide 77

Slide 77 text

val animationBundle = intent .getBundleExtra(EXTRA_EXIT_ANIMATION_BUNDLE) enterAnimationRes = animationBundle .getInt(BUNDLE_ENTER_ANIMATION_RESOURCE) exitAnimationRes = animationBundle .getInt(BUNDLE_EXIT_ANIMATION_RESOURCE) 起動先

Slide 78

Slide 78 text

別アプリの リソースID

Slide 79

Slide 79 text

val resources = packageManager .getResourcesForApplication(packageName) val icon = resources.getDrawable(id, theme)

Slide 80

Slide 80 text

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() } 起動先

Slide 81

Slide 81 text

getPackage()を書き換えると 他アプリのリソースで 終了アニメーションが実現

Slide 82

Slide 82 text

任意のUI(View)

Slide 83

Slide 83 text

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; }

Slide 84

Slide 84 text

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; }

Slide 85

Slide 85 text

val remoteViews: RemoteViews? = intent.getParcelableExtra(EXTRA_REMOTEVIEWS) val remoteViewsClickableIDs: IntArray? = intent.getIntArrayExtra(EXTRA_REMOTEVIEWS_VIEW_IDS) val remoteViewsPendingIntent: PendingIntent? = intent.getParcelableExtra(EXTRA_REMOTEVIEWS_PENDINGINTENT) 起動先

Slide 86

Slide 86 text

RemoteViews

Slide 87

Slide 87 text

val views = remoteViews.apply(applicationContext, toolbar) toolbar.addView(views) reader.remoteViewsClickableIDs?.forEach { views.findViewById(it)?.setOnClickListener { v -> sendPendingIntentOnClick(pendingIntent, v.id) } } 起動先

Slide 88

Slide 88 text

val views = remoteViews.apply(applicationContext, toolbar) toolbar.addView(views) reader.remoteViewsClickableIDs?.forEach { views.findViewById(it)?.setOnClickListener { v -> sendPendingIntentOnClick(pendingIntent, v.id) } } 起動先

Slide 89

Slide 89 text

RemoteViews •Viewの構築情報をParcelableにしたもの •applyしてしまえば、ただのView •標準で使える仕組みは強い •表現力が物足りないなどであれば独自の 仕組みを作るのもありかも

Slide 90

Slide 90 text

オプションメニュー

Slide 91

Slide 91 text

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); } ... }

Slide 92

Slide 92 text

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); } ... }

Slide 93

Slide 93 text

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); } ... }

Slide 94

Slide 94 text

fun makeMenuParamsList(intent: Intent): List { return intent .getParcelableArrayListExtra(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)) } 起動先

Slide 95

Slide 95 text

fun makeMenuParamsList(intent: Intent): List { return intent .getParcelableArrayListExtra(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)) } 起動先

Slide 96

Slide 96 text

fun makeMenuParamsList(intent: Intent): List { return intent .getParcelableArrayListExtra(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)) } 起動先

Slide 97

Slide 97 text

PendingIntent

Slide 98

Slide 98 text

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) }

Slide 99

Slide 99 text

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) }

Slide 100

Slide 100 text

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) }

Slide 101

Slide 101 text

Intent ≠ Parcelable PendingIntent = Parcelable

Slide 102

Slide 102 text

まとめ •プロトコルの宣言は intent-filter • Intentは基本中の基本、使い方を極めよう • AIDLは非常に強力だが利用は慎重に • Parcelable/Bundle超重要、使い方を極めよう

Slide 103

Slide 103 text

Thank you