Chrome Custom Tabs
の仕組みから学ぶ
プロセス間通信
大前良介(OHMAE Ryosuke)
DroidKaigi 2019
自己紹介
大前良介(OHMAE Ryosuke)
https://github.com/ohmae
ヤフー株式会社
Androidアプリエンジニア
Yahoo! JAPAN Yahoo!ブラウザー buzzHOME
Chrome Custom Tabs
Chrome Custom Tabs
使ったことありますか?
質問
Chrome Custom Tabs
アプリ内ブラウザのような見た目で
Chromeを呼び出すことができる
使い方は?
ライブラリ
•SupportLibrary
com.android.support:customtabs
•Jetpack
androidx.browser:browser
CustomTabsClient.bindCustomTabsService(
context, packageName, this)
...
CustomTabsIntent.Builder(session)
...
.build()
.launchUrl(this, Uri.parse(url))
バインドしてセッション指定
com.android.support:customtabs
val customTabsIntent = CustomTabsIntent.Builder()
...
.build()
customTabsIntent.intent.setPackage(packageName)
customTabsIntent.launchUrl(this, Uri.parse(url))
Intentにパッケージ指定
利用するパッケージの決定方法
https://github.com/
GoogleChrome/custom-tabs-client
/shared/src/main/java/org/chromium/customtabsclient/shared/
CustomTabsHelper.java
String getPackageNameToUse(Context)
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
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);
}
}
ブラウザアプリのリスト
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を持つサービス
使用されるアプリは
ブラウザ
かつ
Custom Tabsサービス
を持つアプリ
Chromeだけが
選ばれるわけじゃない
Firefox Custom Tabs
他にもいっぱい対応ブラウザ
https://github.com/ohmae/
custom-tabs-sample
・・・ってことは
アプリを限定しない
汎用的なプロトコル!?
CustomTabs対応ブラウザ
作ってみました
https://github.com/ohmae/custom-tabs-browser
Android的正攻法な
プロセス間通信の
ノウハウの宝庫
本日の内容
Custom Tabsで使われている
Android的正攻法な
プロセス間通信を紹介
基本中の基本だけど奥深い!
Androidのプロセス間通信
•Intent
• startActivity/startActivityForResult
• startService/sendBroadcast
•AIDL(Android Interface Definition Language)
•Content Provider
•その他(Socket通信・ファイル共有・etc.)
Custom Tabsで使われるプロセス間通信
•Intent
• startActivity/startActivityForResult
• startService/sendBroadcast
•AIDL(Android Interface Definition Language)
•Content Provider
•その他(Socket通信・ファイル共有・etc.)
プロトコルに対応した
アプリを探す
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);
}
}
PackageManager
#queryIntentActivities
#queryIntentServices
#resolveActivity
#resolveService
等をつかって探す
AndroidManifest
<activity android:name=".CustomTabsActivity">
<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>
<service android:name=".CustomTabsConnectionService">
<intent-filter>
<action android:name=
"android.support.customtabs.action.CustomTabsService"/>
</intent-filter>
</service>
プロトコルの宣言は
intent-filter
AndroidManifestは
アプリの仕様宣言
AIDL
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) {}
}
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
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
}
起動先
複雑なので簡単な例で
AIDL
interface IRemoteService {
String sendMessage(String message);
}
起動先 class MainService : Service() {
override fun onBind(intent: Intent): IBinder {
return object : IRemoteService.Stub() {
override fun sendMessage(message: String): String {
return message
}
}
}
}
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)
別プロセスの
メソッドをコール
AIDL - メリット
•アプリ間でメソッドコールが可能
•引数・戻り値ともに使える
•データ型も柔軟
•プリミティブ型・String・Parcelable
•それらを要素とするList・Map
•比較的大きなデータも送受信可
AIDL - 注意点
•バインド先プロセスが生きている必要あり
•DeadObjectException
•スレッドセーフに作る
•アプリをまたいだメソッドコールはBinderス
レッドで実行される
•密結合にならないようによく考えて
•一度リリースしてしまうと修正ほぼ不可
Intent
CustomTabsIntent
ってどんなIntent?
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtras(bundle);
intent.putExtra(...)
intent.setData(uri);
context.startActivity(intent, startAnimationBundle);
URIを開くIntent
+
Extra
Intent経由で渡せるパラメータ
•Action(String)
•Category(String)
•Data(Uri)
•Extra(Bundle)
Intent経由で渡せるパラメータ
•Action(String)
•Category(String)
•Data(Uri)
•Extra(Bundle)
ツールバー色・クローズアイコン
public Builder setToolbarColor(@ColorInt int color) {
mIntent.putExtra(EXTRA_TOOLBAR_COLOR, color);
return this;
}
val toolbarColor: Int =
intent.getIntExtra(EXTRA_TOOLBAR_COLOR, Color.WHITE)
起動元
起動先
public Builder setToolbarColor(@ColorInt int color) {
mIntent.putExtra(EXTRA_TOOLBAR_COLOR, color);
return this;
}
val toolbarColor: Int =
intent.getIntExtra(EXTRA_TOOLBAR_COLOR, Color.WHITE)
起動元
起動先
public Builder setCloseButtonIcon(@NonNull Bitmap icon) {
mIntent.putExtra(EXTRA_CLOSE_BUTTON_ICON, icon);
return this;
}
val closeIcon: Bitmap? =
intent.getParcelableExtra(EXTRA_CLOSE_BUTTON_ICON)
起動元
起動先
public Builder setCloseButtonIcon(@NonNull Bitmap icon) {
mIntent.putExtra(EXTRA_CLOSE_BUTTON_ICON, icon);
return this;
}
val closeIcon: Bitmap? =
intent.getParcelableExtra(EXTRA_CLOSE_BUTTON_ICON)
起動元
起動先
プリミティブ型
Parcelable
そのまま使える
TransactionTooLargeException
大きなデータを渡すことはできない
注意
コールバック
CustomTabsIntent.Builder(session)
...
.build()
.launchUrl(this, Uri.parse(url))
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);
}
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);
}
public final class CustomTabsSession {
...
/* package */ IBinder getBinder() {
return mCallback.asBinder();
}
IBinderをExtraで
渡すことができる
val callback: CustomTabsCallback? = try {
CustomTabsSessionToken
.getSessionTokenFromIntent(intent)?.callback
} catch (e: Exception) {
null
}
単純な例
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
起動元
起動先
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
ブラウザモード
CustomTabsモード
Activityを分ける
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;
}
起動元
起動先
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;
}
起動元
起動先
Intent-filterには
Extraの条件をつけられない
<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
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
}
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
}
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
}
アニメーション
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))
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);
}
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);
リソースID
val animationBundle = intent
.getBundleExtra(EXTRA_EXIT_ANIMATION_BUNDLE)
enterAnimationRes = animationBundle
.getInt(BUNDLE_ENTER_ANIMATION_RESOURCE)
exitAnimationRes = animationBundle
.getInt(BUNDLE_EXIT_ANIMATION_RESOURCE)
起動先
別アプリの
リソースID
val resources = packageManager
.getResourcesForApplication(packageName)
val icon = resources.getDrawable(id, theme)
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()
}
起動先
getPackage()を書き換えると
他アプリのリソースで
終了アニメーションが実現
任意のUI(View)
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;
}
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;
}
val remoteViews: RemoteViews? =
intent.getParcelableExtra(EXTRA_REMOTEVIEWS)
val remoteViewsClickableIDs: IntArray? =
intent.getIntArrayExtra(EXTRA_REMOTEVIEWS_VIEW_IDS)
val remoteViewsPendingIntent: PendingIntent? =
intent.getParcelableExtra(EXTRA_REMOTEVIEWS_PENDINGINTENT)
起動先
RemoteViews
val views = remoteViews.apply(applicationContext, toolbar)
toolbar.addView(views)
reader.remoteViewsClickableIDs?.forEach {
views.findViewById<View>(it)?.setOnClickListener { v ->
sendPendingIntentOnClick(pendingIntent, v.id)
}
}
起動先
val views = remoteViews.apply(applicationContext, toolbar)
toolbar.addView(views)
reader.remoteViewsClickableIDs?.forEach {
views.findViewById<View>(it)?.setOnClickListener { v ->
sendPendingIntentOnClick(pendingIntent, v.id)
}
}
起動先
RemoteViews
•Viewの構築情報をParcelableにしたもの
•applyしてしまえば、ただのView
•標準で使える仕組みは強い
•表現力が物足りないなどであれば独自の
仕組みを作るのもありかも
オプションメニュー
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);
}
...
}
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);
}
...
}
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);
}
...
}
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))
}
起動先
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))
}
起動先
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))
}
起動先
PendingIntent
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)
}
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)
}
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)
}
Intent ≠ Parcelable
PendingIntent = Parcelable
まとめ
•プロトコルの宣言は intent-filter
• Intentは基本中の基本、使い方を極めよう
• AIDLは非常に強力だが利用は慎重に
• Parcelable/Bundle超重要、使い方を極めよう
Thank you

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