ChromeCustomTabsでできること～ダークテーマ対応～

今更の話題ですが、CustomTabsIntentにダークテーマ用メソッドが増えていることに気づき、どういうことができるのかを今一度整理してみようと思います。

ダークテーマ対応のメソッドについては、androidx.browser:browser:1.2.0で追加されています。
https://developer.android.com/jetpack/androidx/releases/browser#1.2.0

また、従来ツールバーの色を設定するために使用していた`setToolbarColor`などのメソッドがDeprecatedになっており、代わりに`setDefaultColorSchemeParams`などの新メソッドを使うことが推奨されるように変わってきています。

検証用のアプリは[こちら](https://github.com/ohmae/custom-tabs-sample)、また、CustomTabsのインターフェースを実装した、というか、それ以外の機能の無いブラウザーのソースコードは[こちら](https://github.com/ohmae/custom-tabs-browser)。いずれもMIT Licenseで公開していますのでご参考まで。

また、CustomTabsの機能の中には説明が全く無い、もしくは、あってもコメント程度で、どうやって使うのか分からないメソッドや、どういう意味があるのか分からないコールバックといったものが結構あります。将来の拡張のためなのか、Chromiumのソースコードを調べても未実装だったというものもあります（調べたのが結構前なので今どうなっているか分かりませんが）。その辺は詳細不明と書いていますのでご容赦ください。

# 基本的使い方

詳細は省略しますが、基本的な使い方を抑えておきます。

## サービスのバインド

プロセス間通信による機能を使う場合、サービスのバインドを行います。
単にCustomTabsのUIを使いたいだけの場合、これは必須ではありません。

```kotlin
object CustomTabsHelper : CustomTabsServiceConnection() {
    fun bind(context: Context, packageName: String) {
        CustomTabsClient.bindCustomTabsService(
            context.applicationContext,
            packageName,
            this
        )
    }

    override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
        client.warmup(0)
        session = client.newSession(null)
    }
}
```

## ブラウザーの起動

上記、サービスのバインドを行い、sessionを取得している場合は、それをBuilderに渡すことで、バインドしているブラウザーアプリを起動することができます。

```kotlin
CustomTabsIntent.Builder(session)
    // 様々なオプション設定
    .build()
    .launchUrl(context, url)
```

sessionがない場合も、起動したいブラウザーのパッケージ名を直接指定することで、CustomTabsの呼び出しをすることができます。

```kotlin
val customTabsIntent = CustomTabsIntent.Builder()
    // 様々なオプション設定
    .build()
customTabsIntent.intent.setPackage(browserPackageName)
customTabsIntent.launchUrl(context, url)
```

# UIのカスタマイズ

CustomTabsIntent.Builderに対して様々な設定を行うことで表示されるUIのカスタマイズを行うことができます。

|メソッド|意味|
|---|---|
|setToolbarColor(color)|ツールバーの背景色を指定することができます。ステータスバーも同じ色に設定されます。フォアグラウンドの色は自動的に選ばれ、白に対するコントラストが3以上あれば白、そうでなければ黒が選ばれます。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/875cf469-8e29-d9f6-7dc8-34253f544386.png)![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/a1241254-87ad-b653-4fa1-fe63cdbbcf56.png)|
|setSecondaryToolbarColor(color)|addToolbarItemを設定したときに現れるボトムバーの背景色を指定します。指定しない場合はtoolbarColorと同じ色が設定されます。ただし、addToolbarItemはDeprecatedです。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/6934491c-05a8-a952-708d-bea7a2ac74d7.png)|
|setNavigationBarColor(color)|ナビゲーションバーの色を指定します。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/2e202968-e75c-b18b-1286-6ef1be1d14df.png)|
|showTitle(showTitle)|サイト名(title)を表示するかどうかを設定します。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/950d8c60-25c2-dd13-dd4b-d6f99b4556ad.png)|
|enableUrlBarHiding()|ツールバーをスクロールに合わせて動かすように指定します。設定しなくても動いている気がしますが。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/9c7b19fa-da47-5739-0516-14e5ccc15591.png)![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/756980eb-77f2-f2e1-be37-4b32b51a17f3.png)|
|setCloseButtonIcon(icon)|デフォルトでは×ボタンが表示されますが、別のアイコンを表示させることができます。渡すことができるのはBitmapです。Intentで渡すことになるためサイズに注意。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/d4c2cfd7-ee46-2ed8-3c00-4cfe86874e48.png)|
|addDefaultShareMenuItem()|メニューに共有機能を追加します。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/61c039d7-c4c7-8555-3833-b502fe8081e0.png)|
|addMenuItem(label, pendingInten)|メニューに独自項目を追加します。第二引数にはメニューが選択されたときに送信してほしいIntentをPendingIntentとして渡します。送信されるIntentのdataには選択されたときに表示しているページのURLが格納されています。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/dec819f8-bed2-7d9e-5316-f9b58f0bde92.png)|
|setStartAnimations(context, enterResId, exitResId)|ブラウザを起動するときのアニメーションを指定します|
|setExitAnimations(context, enterResId, exitResId)|ブラウザを終了するときのアニメーションを指定します|
|setActionButton(icon, description, pendingIntent, shouldTint)|ツールバーにボタンを追加します。shouldTintをtrueにすると、フォアグラウンドカラーでtintが設定されます。descriptionはアクセシビリティのためのもので直接表示はされません。タップ時に送信してほしいIntentをPendingIntentとして渡します。送信されるIntentのdataには選択されたときに表示しているページのURLが格納されています。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/8645c4f9-5cfe-4dc2-227a-9140af9bc561.png)|
|addToolbarItem(id, icon, description, pendingIntent)|ボトムバーにアイコンを表示させます。Deprecatedのため新規では利用しない方が良いでしょう。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/6934491c-05a8-a952-708d-bea7a2ac74d7.png)|
|setSecondaryToolbarViews(remoteViews, clickableIDs, pendingIntent)|addToolbarItemの代わりにボトムバーを設定する新しいメソッドです。UIはRemoteViewsで指定することができるので自由度が高くなっています。クリック時の反応についてはRemoteViews#setOnClickPendingIntent()を使うのではなく、第二引数でクリック可能なViewのIDを指定し、そのときに送信して欲しいIntentをPendingIntentとして渡します。クリックされたViewのIDがCustomTabsIntent.EXTRA_REMOTEVIEWS_CLICKED_IDをkeyとするExtraにint値で格納されます。送信されるIntentのdataには選択されたときに表示しているページのURLが格納されています。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/1d991500-4bc8-2afb-1599-4d24f7b752d9.png)|
|setColorScheme(colorScheme)|ダークテーマの指定を行います、CustomTabsIntent# COLOR_SCHEME_SYSTEM / COLOR_SCHEME_LIGHT / COLOR_SCHEME_DARK が指定でき、順にシステムに従う、ライトテーマ、ダークテーマです。COLOR_SCHEME_SYSTEM は当然OS側がダークテーマに対応していないと切り替わりません。COLOR_SCHEME_LIGHT / COLOR_SCHEME_DARKはブラウザーが対応していればOSが対応していなくてもWebページのレンダリングも含めて切り替わります。 ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/ac98971c-f9fe-7b23-a2e4-5441de70de0f.png)|
|setColorSchemeParams(colorScheme, params)|toolbarColor / secondaryToolbarColor / navigationBarColor を colorSchemeに応じて変更したい場合に指定します。setColorSchemeでCOLOR_SCHEME_SYSTEM を指定した場合に、システムテーマがライトかダークかで色を変えたい場合ですね。第二引数はCustomTabColorSchemeParamsというクラスでBuilderを使ってそれぞれの色を設定します。この機能はブラウザー側が対応していない可能性もあるので、デフォルトの色をsetToolbarColor等で設定した上で、COLOR_SCHEME_DARKなどテーマで上書きしたい色を設定するように指定することが推奨されています。 ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/5732013e-0522-73f8-c811-6ccfc892bbe5.png) |

# サービスの機能

あまりクローズアップされることはないですが、結構需要な機能があります。

## CustomTabsClient

|メソッド|機能|
|---|---|
|warmup(flag)|ブラウザーアプリに起動の準備をリクエストします。起動前の初期化までを先に行わせておくことで、起動が速くなります。long型引数がありますが、「Reserved for future use.」とのことなので適当に0とかを入れておけば良いです。|
|newSession(callback)|ブラウザーアプリとの間でセッションを作成します。引数のcallbackはNullableなので必要なければnullで問題無いです。Callbackを渡せば様々なイベントを受け取ることができるようになります。|
|extraCommand(command, args)|「Can be used as a channel between the Custom Tabs client and the provider to do something that is not part of the API yet.」らしいです。ブラウザーとアプリの提供元で連携すれば何かに使えるのでしょうか。詳細不明|

コールバックについては別途説明します。それ以外で特に詳細を説明できそうなものもないので次に行きます。

## CustomTabsSession

|メソッド|機能|
|---|---|
|mayLaunchUrl(url, extras, otherLikelyBundles)|これから要求する可能性のあるURLを伝えます。先読みを行うなどで読み込みを高速化させることができます。|
|requestPostMessageChannel(postMessageOrigin)|「Sends a request to create a two way postMessage channel between the client and the browser.」らしいです。詳細不明|
|postMessage(message, extras)|前述のメソッドで作ったチャンネルを使ってメッセージを送信する、同様に詳細不明|
|validateRelationship(relation, origin, extras)|Digital Asset Linksを使って呼び出し元アプリとoriginの間の関係性の検証をリクエストする。多分AppLinksの検証をリクエストするってことでしょうけど、よく分かりません。|
|receiveFile(uri, purpose, extras)|Intentでは渡せないような、大きなファイルを渡すらしいですが、詳細不明|
|setActionButton(icon, description)<br>setSecondaryToolbarViews(removeViews, clickableIDs, pendingIntent)<br>setToolbarItem(id, icon, description)|後述のUI設定と同じメソッドの一部が用意されており、動的にUIを変更することができるようです。具体的なユースケースが分からず試していないです。|

ってことで不明だらけで、分かっているのはmayLaunchUrlぐらいでしょうか？

mayLaunchUrlの第一引数はUriで、開く可能性の一番高いURLを渡します。第二引数はreserveなのでnullを渡しておきましょう。第三引数に第一引数以外に伝えたいURLを渡しますが、型がList<Bundle>です。このBundleはkeyをCustomTabsService.KEY_URLとして、UriをParcenableとして格納したBundleです。正直なぜUriのリストにしなかったのかと疑問ですが、以下のように詰め替えして渡します。

```kotlin
session.mayLaunchUrl(
    Uri.parse(url),
    null,
    listOf(...)
        .map { Uri.parse(it) }
        .map { Bundle().also { it.putParcelable(CustomTabsService.KEY_URL, it) } }
)
```

## CustomTabsCallback

CustomTabsClient#newSession() に渡すコールバックです。様々なイベントを受け取ることができます。

|メソッド|機能|
|---|---|
|onNavigationEvent(navitationEvent, extras)|ブラウザーで発生したイベントが通知されます。定義されているイベントについては後述。extrasにはkey:`timestampUptimeMillis`でイベント発生のタイムスタンプとおぼしき値が入っています。|
|extraCallback(callbackName, args)|任意のコールバック、CustomTabsSession#warmup()の結果、`onWarmupCompleted`（引数null）、メニューからブラウザーで開く、をしたときに`onOpenInBrowser`（引数：timestampUptimeMillis）が呼び出されました。|
|extraCallbackWithResult(callbackName, args)|結果を返す任意のコールバック呼び出しですが、詳細不明|
|onMessageChannelReady(extras, extras)|CustomTabsSession#requestPostMessageChannel()の結果コールされるのだと思いますが、詳細不明|
|onPostMessage(message, extras)|CustomTabsSession#postMessage()関連だとは思いますが、詳細不明|
|onRelationshipValidationResult(relation, requestedOrigin, result, extras)|CustomTabsSession#validateRelationship()の結果呼び出されるらしいですが、詳細不明|

onNavigationEventのイベントとしては以下が定義されています。

|イベント|意味|
|---|---|
|NAVIGATION_STARTED|読み込み開始|
|NAVIGATION_FINISHED|読み込み完了|
|NAVIGATION_FAILED|読み込み失敗|
|NAVIGATION_ABORTED|ユーザー操作により読み込みが中断（リンククリックやリロードなど）|
|TAB_SHOWN|タブが表示された|
|TAB_HIDDEN|タブが非表示になった|

----

ということでざっとまとめました。
プロセス間通信周りは不明なところが多くすみません。詳細をご存じの方がいらっしゃればコメント等で教えていただければと思います。