こんにちは、 CL 事業部の @takasfz です。

CL は本日、 TV デバイスに対応しました!すでに Android のモバイルアプリをリリース・稼働している中で、新たに Android TV デバイスに対応するために行ったことを紹介します。

目次

  1. TV 用のバージョンコードを用意する
  2. モバイル / TV 両対応のモジュール構成にする
  3. VRT に対応する
  4. TV 用の APK 配布方法を用意する
  5. 番外編: クローズドテスト以外の方法でテストを済ませておく

TV 用のバージョンコードを用意する

CL は、モバイルと TV を別々の APK として公開しています。複数の APK を同一のアプリとして公開するためには、各 APK に異なるバージョンコードを設定する必要があるため、バージョンコードスキームを新たに制定しました。

公式ドキュメントでは、バージョンコードスキームとして 7 桁の数字を使う例を紹介しています。最初の 2 桁は API レベル用、3 桁目と 4 桁目は機能や特性 ( ここでは画面サイズ ) 、最後の 3 桁はアプリバージョン用です。

7 桁のバージョンコードスキームの例が二つ。一つは、ゼロヨン、イチニ、サンイチゼロ。もう一つは、イチイチ、サンヨン、サンイチゼロ。
バージョンコードのスキーム例 ( 出典: Android 公式ドキュメント )

しかし、この例をそのまま採用するのはいくつか問題がありました。

一つは、公開している APK の一部だけ API レベルを上げるケースに対応していない点です。例えば、この例の画面サイズが 12 である APK だけ API レベルを 14 に上げようとすると 14_12_310 となり、画面サイズが 34 である APK とバージョンコードの大小関係が逆転してしまいます。 2 つの APK 間でサポート対象範囲に重複がある場合はバージョンコードが高い方の APK がインストールされるため、大小関係が逆転すると、これまで 34 の APK を使っていたデバイスに 12 の APK がインストールされてしまう可能性があります。

もう一つは、アプリバージョンをセマンティックバージョニングの 3 桁の数字で表している点です。この方法では、アプリが 1.10.0 のようなバージョンになったときに対応できません。

そこで CL では、8 桁のバージョンコードスキームを制定しました。最初の 2 桁は機能や特性、3 桁目と 4 桁目は API レベル用、最後の 4 桁はビルド番号用です。

CL のバージョンコードスキーム
00 00 0000
意味 機能・特性 API レベル ビルド番号
変更頻度 変わらない 稀に変わる 毎回変わる

バージョンコードの大小関係の逆転を防止するために、変更頻度の低いものほど上の桁に持ってきています。また、機能・特性のバリエーションを増やす余地を残すために、 01, 02 のような連番ではなく、 10: TV、20: モバイル を割り当てました。

アプリバージョンは 4 桁のビルド番号とし、セマンティックバージョニングの桁数の影響を受けないようにしました。

モバイル / TV 両対応のモジュール構成にする

CL は機能単位でモジュールを分割するマルチモジュール構成としており、これまでは 1 つの機能が 1 つのモジュールに対応していました。

TV デバイスに対応するにあたって モバイル / TV の両方で利用するコードを shared モジュールに切り出し、それぞれのデバイスでのみ利用するコードは独立したモジュールとして、機能ごとに shared / mobile / tv の 3 つのモジュールを持つ構成にリファクタリングをしました。

モジュールを分割する際、どちらかのデバイスがサポートしていない機能 ( feature ) を使う実装が shared モジュールに紛れ込んでしまうとアプリを公開できなくなってしまうため、注意が必要です。 CL では、screenOrientation="portrait" な Activity が shared モジュールに紛れ込んでしまっていることに気付かず、 TV アプリをストアにアップロードした際に、機能要件を満たさないために TV デバイスをサポート対象に追加できない事態となってしまったことがありました。

VRT に対応する

CL では UI のテストに VRT を導入しています。

TV アプリでも VRT を実行できるようにするには、 ViewTree を自動で探索する部分のコードを leanback に対応させる必要があります。 leanback の UI はほとんどが RecyclerView をベースにしているため、以下のように LinearLayoutManager の各メソッドに対応する操作をピュアな LayoutManager で再実装した関数を用意することで、比較的簡単に対応することができました。

// cf. androidx.recyclerview.widget.LinearLayoutManager.findOneVisibleChild
private fun RecyclerView.findVisibleItemPosition(
  fromIndex: Int,
  toIndex: Int,
  completelyVisible: Boolean
): Int {
  // ...
}

fun RecyclerView.findFirstVisibleItemPosition(): Int =
  (layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition()
    ?: findVisibleItemPosition(0, childCount - 1, false)

fun RecyclerView.findFirstCompletelyVisibleItemPosition(): Int =
  (layoutManager as? LinearLayoutManager)?.findFirstCompletelyVisibleItemPosition()
    ?: findVisibleItemPosition(0, childCount - 1, true)

fun RecyclerView.findLastVisibleItemPosition(): Int =
  (layoutManager as? LinearLayoutManager)?.findLastVisibleItemPosition()
    ?: findVisibleItemPosition(childCount - 1, 0, false)

fun RecyclerView.findLastCompletelyVisibleItemPosition(): Int =
  (layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition()
    ?: findVisibleItemPosition(childCount - 1, 0, true)

CI で VRT を実行する際には Firebase Test Lab を利用していますが、 Test Lab で使用できる端末に TV デバイスはありません。 TV に近い解像度を持つものだと Nexus7 clone, DVD 16:9 aspect ratio ( 1280 x 720 ) という端末があるので、これを使ってスクリーンショットを撮るようにしました。

Nexus7 は tvdpi のはずなのですが、この端末は xhdpi で動いていたので、 VRT 実行時に UiAutomation#executeShellCommand で density を合わせるようにしました。

TV 用の APK 配布方法を用意する

TV アプリは Firebase App Distribution が使えないので、代替となる APK 配布の方法を用意する必要があります。

CL では APK 配布用の GCS バケットを用意し、 CI で APK をアップロードするようにしました。先に挙げた VRT 等ですでに GCS を利用していたことと、アクセス権の管理が容易であることが理由です。一定期間経過後に自動で削除されるように設定しておくことで、古い APK が溜まり続けるのを防ぐことができるのも良かったです。

番外編: クローズドテスト以外の方法でテストを済ませておく

CL は通常、アプリのリリースはまずクローズドテストトラックで公開し、問題がないことを最終確認してから製品版にプロモートする流れとしています。しかし、今回新たに TV 用の APK を追加してクローズドテストトラックで公開したところ、テスターにオプトインしていても Play ストアに TV アプリが表示されないという現象が発生しました。

デベロッパーサポートへ問い合わせたところ、「ストアのデバイスサポート判定は製品版を基準に行われるため、製品版でサポートしていないデバイスは、テストトラックでサポートしていてもストアからインストールすることはできない」との回答でした。

今回は大型リリースということもあり、クローズドテストでリリースよりひと足先に部署のメンバーに使ってもらう予定だったので、それができず本当に困りました…。


またアプリのリリースに先立って、 Lounge というライブラリもリリースしました。 Lounge は同じチームの @lcdsmao が作成したライブラリで、 Leanback の UI を Epoxy ライクに実装することができます。 CL のモバイルアプリは Epoxy を利用していて馴染みがあるため、 Lounge を利用することで Leanback UI 実装のハードルが下がり、開発効率がかなり向上しました。

より技術的な内容については、ブログ記事や CA.apk 等で解説してくれることを期待しましょう。


本日のリリースで、 CL は ABEMA / AWA / OPENREC.tv に続いて 4 つ目の TV デバイス対応サービスとなりました。モバイルと比較すると情報の少ない TV 開発において、多くのエンジニアが携わりプロダクト横断で情報共有することができるのは、他にはないサイバーエージェントの強みではないかと思います。

やりたいことはまだまだたくさんあるので、この強みを活かして、これからさらに良くしていきたいと思っています。

2015 年中途入社。iOS / Android / Web の広告 SDK 開発を経て、現在は CL の Android アプリを作っています。