📌

GitHub ActionsのartifactからAndroidにAPKをインストールできるカタログアプリの仕組みを作った

2023/01/23に公開

AAP APK Installer

最近自作のAndroid用オーディオプラグインのプロジェクトをAudio Plugins For Androidという包括的なエコシステムとして打ち出していこうと決めたのだけど(昔から公開はしていて、名前はAndroid Audio Plugin = AAPだった。略称は今でもこのまま)、このプロジェクトは大量のOSSプラグインをインポートして世界観を作り出していくというアプローチを採っていて、既に20件以上のGitHubリポジトリが存在している。これらはまだPlay Storeで公開して役に立つようなものでもクオリティでもないので、GitHub Actionsでartifactを作るところまでしかやっていない。

この多数のプロジェクトは、実のところ自分のAndroidデバイスにはほとんど入っていなくて(普段はだいたいエミュレーターで開発している)、たまにM3みたいなイベントで展示したり他人に見せたりするときに、Android Studioでちまちまとプロジェクトを開いてはデモ機などにデプロイしていた。しかし、さすがに20件もリポジトリがあったらいちいち全部やっていられない。

そういうわけで、多数のGitHubリポジトリのAndroidプロジェクトのActionsのartifactからapk/aabをまとめて入れる仕組みを作ってみた。立ち位置としては、Native InstrumentsのNative Accessみたいなやつだけど、そこまで便利に作り込んではいない。現状、使うためにはGitHubアカウントとPAT (personal access token) が必要になる(GitHubはartifactのダウンロードにアカウントを必要とする)。

https://github.com/atsushieno/aap-ci-package-installer

以降だいたいREADMEにも書いてあるんだけど、英語で長いからちょいちょい解説も含めてここでまとめ直そうと思う。

基本機能

AAP APK Installerは、ハードコーディングされたプラグインリストの中からプラグインを選ぶと、ダウンロード/インストールできる画面が出てきて、ボタンを押すとダウンロードがバックグラウンドで始まって、それが終わるとAndroidシステムの機能でインストールできるようになっている。

GitHubアクセスはGitHub API for Javaでサクッと実現できたのだけど、Android APIのPackageInstallerがいまいち期待通りに動作せず、そもそもAOSPのApiDemosに含まれているPackageInstallerのサンプルもまともに動作しなかったので、今やdeprecatedとなっているIntent.ACTION_INSTALL_PACKAGEを使ったアプローチで実装している。

GitHubアカウント認証はPATで雑に済ませていたのだけど、OSSとして公開するのにPATを突っ込むわけにはいかないので、JWTとかいろいろ模索を経て、単純にユーザー名とPATを入力して使ってもらう仕組みにした。真面目に作り込むならOAuth 2.0でやるべきなんだろうけど、そこまではやっていない。

generic APK installer as a library

作りながら気づいたのだけど、任意のリポジトリのCIビルドのartifactに含まれるapkをカタログでかき集めてボタン1つでインストールできる仕組みは、それなりに汎用性があって、他のプロジェクトでも利用できそうだ。今ダウンロードできるのはGitHubのbuild artifactのみだけど、releasesからダウンロードしてもいいだろうし、GitLab CIやCircleCI、Bitriseでも同じことができるかもしれない。

あるいは、GitHub searchで適当にプロジェクトを検索してターゲットにする…というのだと、packageNameやappLabelが取得できないとちゃんと機能しない可能性があるけど、 プロジェクトにメタデータファイルをチェックインさせておけば取得できる…みたいな仕組みは考えられる。カジュアルにターゲットを広げすぎると、「信頼できないソースからのインストール」が濫用され得る側面があるけど、可能性はいろいろ広げられそうだ。実際、このリポジトリを公開した翌日には「artifactじゃなくてreleaseから取得するようにすればGitHubユーザー認証もいらないのでは?」というメッセージが寄せられている。

ポテンシャルがあることはわかったので、とりあえずAAP APK InstallerというアプリケーションはAndroid CI Package InstallerというライブラリをAAP用に調整したもの、という構成に作り変えた。

再利用を前提としたライブラリにはしたものの、現状、このプロジェクトは自分に必要な機能を実現してしまったので、自分から積極的に追加機能を実装することはないと思う。実のところ実装もかなり安直なのでリファレンス的価値もあんまし無いと思う。

使い方

この仕組みを使ってアプリケーションを配布したい場合は、現状ライブラリをMavenで公開したりはしていないので、このリポジトリ自体をforkしてそこにappモジュールと同じような感じで別のAndroid applicationモジュールを追加して、appと同じようなコード構成にすればできるはずだ。筆者ならcp -R app [yourApp]でまるっとコピーして、settings.gradleinclude :appの下にinclude :[yourApp]を追加して、build.gradleとかAndroidManifest.xmlvalues.xmlを自分のやつ用に調整して、MainActivity.ktを書き換えて済ませると思う。

既存の仕組みでいいのでは?

当初は、GitHub Actionsのartifactからインストールする仕組みを自作するなんてことは考えていなくて、Firebase TestingやDeployGateに自動的にパッケージを発行する仕組みを作ればいけるのでは、と思っていた。でもFirebase Testingだとパッケージ毎にFirebaseプロジェクトを作ることになるし、プロジェクトが20件とかあってメンテナンスしきれないから管理したいわけで、同じだけFirebaseプロジェクトを管理するなんて考えたくもない。

DeployGateでも同じことが言えそうなんだけど、こっちはfree tierが2プロジェクトまでしか試せなくて、有料のサービスを使うことに個人的な抵抗感は無いんだけど、プロジェクト自体はOSSで回しているので、いざとなったら自分でなくても回せることが望ましいと思ってやめといた。ただ、AAP APK Installer自体はひとつのアプリとして配布できるので、DeployGateで公開してみている:

https://dply.me/vl8vfr

ちなみに、AAP APK InstallerのカタログにはAAP APK Installer自体が含まれているのだけど、これがちゃんと機能しているのかは現時点ではわかっていない。

機能的な制限

パーミッションの問題

このインストーラーはある意味「ストアアプリ」みたいなもので、Google Play Storeや他のベンダーマーケットのストアアプリみたいに機能するものが作れれば良かったんだけど、現実にはそうはできない側面がある。ひとつには、アプリケーションのインストールにはユーザーの明示的な同意操作が必要ということだ(これがなかったら危なっかしいということはわかってもらえるだろう)。

ストアアプリの類はバックグラウンドでインストールやアップデートをユーザーインタラクションなしで実行できるのだけど、これはandroid.permission.INSTALL_PACKAGESというパーミッションに基づいていて、このパーミッションは「システムアプリケーション」にしか付与できない。システムアプリケーションを発行できるのはAndroid OSをパッケージしているベンダーなので、一般のアプリケーションの開発者にはこれはできない。一般のアプリケーション開発者が設定できるパーミッションはandroid.permission.REQUEST_INSTALL_PACKAGESまでだ。「リクエストできる」ことまでしかできなくて、実際にインストールするためにはユーザーの明示的な許可が必要になる。

そういうわけで、Native Accessのように「プロダクトをチェックボックスで選択してインストール指示したら後は勝手にやってくれる」みたいな操作はできない。PackageInstallerだと"split apks"がサポートされていて複数のAPKをインストールすることもできるように見えるけど、実際にはひとつのパッケージの名の下に機能を分割しているだけなので、今回のユースケースには相当しない(AAPは「いろんなプラグインベンダーがいろんなプラグインを公開する」前提なので、「公式APKが全プラグインを代表して配布する」ような仕組みはありえないし、自分のパッケージだけそれらをまとめても意味がない)。

API Level 33以降ではPackageInstaller.SessionParamssetRequireUserAction()というメソッドが用意されていて、これが有効なときは確認ダイアログが出ない可能性はあるけど、OSの挙動次第だと思うし、いずれにせよPackageInstallerの動作する例が無いので実験もできないところだ。

パッケージ詳細情報の取得

このインストーラーは現状PackageInstallerのAPIを使っていないので、パッケージの詳細情報を事前に把握している必要はない(apkやaabのバイナリだけあれば足りるし、名前がマッチしなくてもインストールできてしまう)のだけど、現実的にはインストールするパッケージのpackageNameくらいは把握しておきたいだろう。もちろん普通にパッケージインストールリクエストのダイアログではシステムのインストーラーがアプリケーション名などを表示してくれるはずだ(packageNameまで表示するかはシステム次第だ)。PackageInstallerを使うなら、少なくともPackageInstaller.SessionParamsを構築するためにpackageNameappLabelが必要になる。originatingUrl(ソースのリポジトリ)などもほしいところだ。

APKはbuild artifactとしてダウンロード可能なので、パッケージの詳細情報は機械的に取得することもできないわけではない。しかし、APKに含まれるパッケージ情報は、APKをダウンロードしてみないとわからないので、起動画面でリストアップするときにGitHubにクエリして落としてくるわけにもいかない。そういうわけで、「カタログ」は手作業で埋めるか、あるいは何らかの代替手段で取得するのが適切だろう。GitHub searchでプロジェクトを検索して引っ張ってきても、それだけではパッケージ詳細情報は取得できないので、他の手段が必要になる。リポジトリにpom.xmlみたいなノリで所定のメタデータを入れておいてもらう、みたいなことが考えられる。アイディアはあるけど自分で実装するかはわからない。

バージョン/セット管理

AAPはAPIが安定していないオーディオプラグインのエコシステムであり、ホスト-プラグイン間のプロトコルはなるべくいじらないようにとは考えているものの、現状では機能が揃っているわけでもないので、変更が生じることは避けられない。またパッケージソリューションとしてのAAPを考えれば、関連プロダクトごとに独自のエコシステムをIntent URIごとに用意して運用するというのが安牌だろう。バージョンが上がって安定化してくれば、LTS的なIntent URIを用意することも多分できるようになる。

これを可能にするためには、ひとつにはこのアプリ自体をエコシステムごとに別々のパッケージとして提供するアプローチも考えられるが、ひとつのアプリ内で対応エコシステムごとに切替可能にしてもいい。GitHub Actions artifactから入れるのとReleaseから入れるのも別々のエコシステム扱いにして…と考え始めると複雑化しすぎるかもしれないので(現状ではbuild artifactからしかインストールできないわけだし)、あまり厳密な管理を考えることはないと思うけど、何らかの利便性を確保したいところではある。

Thoughts?

…という感じで、なさそうだなあと思うものを作ってみたわけだけど、もしかしたらもっと良いソリューションがあるのかもしれない(一応ざっくりGitHubとかでそういうソリューションは無いのかと探してみたけど特に見当たらなかった)。ここまで読んでいて何か思いついたことがあったらコメント欄などで教えてください。

Discussion