💡

オープンなアプリケーションストアを改善する? PackageInstallerとそのアップデート

2023/05/28に公開

技術書典14向けに書いていた原稿ですが、TechBoosterの新刊の企画がお蔵入りになってしまったので、ここで供養します。mhidaka編集マジックの加えられていない原稿をおたのしみください。[1]


ほとんどのAndroidアプリケーション開発者にとって、アプリケーションの配布は、Google Play Storeを経由して行うのが最も効率的な手段であることに間違いはないでしょう。しかし、Google Play StoreはGoogleのプロプライエタリなサービスであり、Googleが自社のポリシーに反するアプリケーションを配布できないアプリケーションストアです。Androidは(少なくとも建前上は)ユーザーが自由にアプリケーションをインストールできるプラットフォームであり、独自のアプリケーションストアや独自にアプリケーションを配布する仕組みを構築できます。

独自のアプリケーション配布機構を構築できるといっても、ユーザーの承認を確実に得られるようにするために、AndroidではアプリケーションをインストールするAPIやシステム設計に制限を設けています。Google Play StoreやAndroidデバイスのベンダーが提供するアプリケーション・ストア[2]以外の方法で、任意のアプリケーションをインストールするソフトウェア上の手段は、Android 14の時点では、次の3つになります。

  • ADB(Android Debug Bridge)を使用してホストからアプリケーションをインストールする
  • Androidシステム上のインストーラー サービスをIntent経由で操作する(API Level 29から非推奨)
  • PackageInstaller APIandroid.content.pm.PackageInstaller)を使う

ADBを使用するやり方は、企業や学校などでAndroidデバイスを一定の目的に沿って管理できる場合には採用可能な手段ですが、USB経由でデバッグ接続を許可されたホストからインストールするというものであり、一般のアプリケーション開発者が採用できる手段ではありません。Androidシステム上のインストーラー サービスにIntentを送るやり方は、現在でも利用可能なアプローチではあるものの、Android 10で廃止とされたもので、今後いつまで利用可能であり続けるかは不透明です。

このような状況では、PackageInstaller APIを使用するのが一番率直な(見方によってはほぼ唯一の)選択肢ということになるでしょう。このPackageInstaller APIですが、実はその使い方については割と参考資料がなく、その割に意外とハマりどころがあるAPIです。この章ではこのPackageInstallerの基本的な使い方を、Android 14で加えられたアップデートまで含めて解説します。

2020年代初頭のGoogleとAppleは、そのアプリケーションストアの独占性が日本を含む各国で問題視されており、すでに名目上はオープンなアプリケーションストアを実現可能なAndroidは、これが実質的にも競争的に実現可能であることが求められてきています。iOS17にはApp Sideloadingが実装されていてEU圏で公開されるといわれています[3]。Android 14のPackageInstallerは、そのような状況で各種ベンダーや行政からのフィードバックを受けて改善されてきた成果と考えられています[4]

従来型のプリミティブなシステムインストーラーの操作

Androidアプリケーションのインストールには、Androidシステム上で動作しているインストーラーデーモンinstalldを操作するシステムサービスcom.android.server.pm.PackageManagerServiceとのやり取りが必要になります。これは従来型の(廃止される)Intentを使ったアプローチでもPackageInstallerを使ったアプローチでも変わりません。

この前提を踏まえて見直すと、Intentを使ったアプローチはストレートに作られています。Intentを使用したアプローチによる実装例(Kotlin)を見てみましょう(なお、話を簡単にするために、パーミッションについては既に処理済みであるとして、後できちんと解説します)。

val file: File = getApkFile() // どこかで実装する
val intent = Intent(Intent.ACTION_INSTALL_PACKAGE)
intent.data = FileProvider.getUriForFile(context,
        context.packageName + ".fileprovider", file)
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
intent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.packageName)
val activity: Activity = getActivityBySomeMeans() // どこかで実装する
activity.startActivity(intent) // [^start-activity]

この仕組みでは、システムのインストーラーに対して、インストールするapkのデータを、.dataプロパティに指定するURIで渡す必要があります。ここでは一時保存したファイルのURIを作成して渡しています。このURIのファイルをインストーラーが読めるようにするには、何らかのcontent providerを使用して自分のアプリケーション以外から読めるようにする必要があります。ここではandroidx.core:coreに含まれるandroidx.core.content.FileProviderを使用する例を示します。AndroidManifest.xmlに次の<provider>要素を追加します。

<application>    
  <provider  
    android:name="androidx.core.content.FileProvider"  
    android:authorities="${applicationId}.fileprovider"  
    android:exported="false"  
    android:grantUriPermissions="true">  
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />  
  </provider>
  ...

これで前述のコードを実行すると、システムのインストーラーにapkを直接送りつけて「後は任せる」処理を実現できます。任されたシステムのインストーラーは、一般的には次のような確認ダイアログをユーザーに提示します。

Androidシステムからユーザーへのインストール確認ダイアログ{scale=0.6}

Intentを受け取る側であるシステム側のインストーラー(PackageManagerServiceinstalld)の仕組みは、それはそれで調べてみると面白いトピックではあり、またインストーラーをデバッグしているとエラーメッセージ等を芽にする機会も多いものですが、Web上に多くの資料が存在するので、この章では詳しく言及しません。

アプリケーションのインストーラーとパーミッション

アプリケーションのインストールは、他のアプリケーションを自動的にインストールされたりアンインストールされたりする可能性がある危険な操作であり、通常のパーミッション操作によってユーザーがカジュアルに承認できるべきものではありません。また、システムのインストーラーがインストールのユーザー承認ダイアログを出してくることを、ユーザーが十分に予期できるべきでもあります。そのため、システムにプリインストールされている「設定」(Settings)アプリケーションで明示的に承認する操作が必要になっています。[5]

PackageInstallerを使用している場合は自動的に処理されますが、Intentを使用した従来型のインストーラーの場合は、この「設定」アプリケーションでインストーラーの項目を設定するActivityをIntentで呼び出すことができます。[6]

val context: Context = getContext() // どこかで実装する
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
                             // Settingsの特定項目を示す定数
intent.data = Uri.parse("package:${context.packageName}")
context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))

未知のアプリケーションをインストールできるパッケージの設定(Settings){scale=0.6}

これに加えて、AndroidManifest.xmlでは明示的にアプリケーションのインストールを「リクエストする」パーミッションを明記する必要があります。なおアプリケーションの削除にも別のパーミッションが必要になります。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <!--インストールに必要-->
  <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
  <!--削除に必要-->
  <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
  ...

あくまでアプリケーションのインストールを「リクエストする」パーミッションであって、実際のインストールに必要なandroid.permission.INSTALL_PACKAGESとは別物であることに注意してください。このパーミッションは、Androidシステム・アプリケーションや、デバイスにプリインストールされたパッケージでのみ、指定可能なものとされます。

PackageInstallerでより高度なインストーラーを作る

ここまでは、主にシステムインストーラーを直接操作するIntentを使ったやり方を説明してきました。残念ながら、冒頭で説明したとおり、このアプローチはAndroid 10以降では非推奨となっています。Android 10以降ではPackageInstaller APIを使うやり方が王道ということになります。このAPIはAndroid 5.0の時点で追加されているので、Android 4.4以前の端末で利用することを考えない限りは問題なく使えます[7]

PackageInstallerは従来型のIntentの仕組みより複雑で、全体的にセッションに基づいてインストール処理を段階的に行うようになっています。このセッションの仕組みは、システムの状態に応じて中断しても問題なく動作するように永続化されるように作られています。

PackageInstaller.Session

インストーラーの開発者は、PackageInstaller.Sessionというクラスを使用してこのセッションを操作することになります。セッションはPackageInstallercreateSession()メソッドによって生成され、sessionIdというint型のIDが設定されてこのメソッドから返されます。PackageInstaller.Sessionのインスタンスは、新規作成したばかりであっても、既に存在していて永続化されているセッションであっても、openSession()によって取得します。

public class PackageInstaller {
    public int createSession(@NonNull SessionParams params) ...
    public Session openSession(int sessionId) ...
    ...

APKファイルのバイトストリームは、Intentの.dataフィールドのようなURIではなく、PackageInstaller.SessionインスタンスのopenWrite()メソッドから返されるjava.io.OutputStreamに書き込むことになります。中断された時に書き込みが破棄されたくない場合は随時、fsync()メソッドを呼び出して、このストリームを永続化しておきます。

APKのストリームを全て出力し終えたら、(fsync()を呼び出して永続化しておいてから)PackageInstaller.Sessionインスタンスのcommit()メソッドを呼び出して、先述のシステム上のインストーラー サービスにIntentを送る処理を行わせます。

public class PackageInstaller {
    public static class Session implements Closeable {
        public OutputStream openWrite(@NonNull String name, long offsetBytes,
            long lengthBytes) ...
        public void commit(@NonNull IntentSender statusReceiver) ...
        ...

システムインストーラーのリアクションに対応する

commit()メソッドは引数にIntentSenderを渡す必要があり、インストーラーがリクエストに対してどう応答を返したかを、Intentの発行というかたちで受け取らせることになります。一番シンプルな対応方法は、BroadcastReceiverを登録しておく方法でしょう。ここでは、PackageInstallerReceiverというBroadcasterの実装クラスを自分で定義して、PendingIntentを使用してintentSenderとして使用する例を示します。

val intent = Intent(context, PackageInstallerReceiver::class.java)  
val pendingIntent = PendingIntent.getBroadcast(context,
    12345, // 何らかのリクエストコード  
    intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)  
session.commit(pendingIntent.intentSender)

いくつか注意点を箇条書します。

  • 定義したBroadcastReceiverのクラスはAndroidManifest.xml<receiver>要素で登録しておきます。android:exported="true"にする必要はありません。
  • PendingIntent.FLAG_MUTABLEはAndroid 12(targetSdk 31)以降で必要になります。内容を書き換えられない場面であればPendingIntent.FLAG_IMMUTABLEも利用可能ですが、PackageManagerServiceはこのIntentにリアクションの内容をextra statusとして追加するので、immutableになっているとintentの内容の更新に失敗し、intent引数の内容がそのまま返されて、正常なインストール処理は続行不能になります

インストーラーのリクエストに対する「応答」は、intentSenderを介して返されてきたIntentPackageInstaller.EXTRA_STATUSで示されるint型のextraフィールドに格納されており、次のように分類できます:

  • PackageInstaller.STATUS_SUCCESS : インストール処理が成功したことを意味します
  • PackageInstaller.STATUS_PENDING_USER_ACTION : インストール処理を続行する前にユーザーの承認が必要であることを意味します
  • その他 : PackageInstaller.STATUS_FAILUREなどで、ほぼエラーと考えて良いでしょう(PackageInstaller.EXTRA_STATUS_MESSAGEで示されるString型のextraフィールドで詳細を確認できるかもしれません)

最初は通常PackageInstaller.STATUS_PENDING_USER_ACTIONのIntentが返ってきます。このIntentには内容としてIntent.EXTRA_INTENTに承認リクエストのUIを表示するためのIntentが含まれているので、Intent.getParcelableExtra()を使用してこれを取得して、context.startActivity()で呼び出します。[8]

override fun onReceive(context: Context, intent: Intent) {
    when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -37564)) {
        PackageInstaller.STATUS_PENDING_USER_ACTION -> {
            val i = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
                ?: throw java.lang.IllegalStateException("No extra intent found")
            context.startActivity(i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
        }
        ...

ここでユーザーがインストールを承認すれば、インストール処理が続行し、その結果で更新されたIntentが呼び出されることになります。ユーザーに却下された場合も同様に、結果が更新されたIntentが呼び出されることになります。

インストール処理を諦める場合はPackageInstaller.Sessionabandon()を呼び出すと、そのセッションは破棄されて、保存されていたストリームのデータなどが全て削除されることになります。ちなみに、これに関連する注意点として、ユーザー承認が却下された場合、筆者が試した範囲では、そのセッションは既に破棄されているので、別途abandon()を呼び出す必要はありません(むしろ二重に呼び出すとSecurityExceptionが発生するでしょう)。

ここまでがPackageInstallerの基本的な使い方となります。Intentを使ったアプローチと比べると、やることが格段に多くなってしまいました。中断と再開なども含めた高度な機能とオプションをサポートしている関係で、全体的な複雑化は不可避ですが、シンプルなワークフローのコードを書いて済ませたいという読者は、GitHub上にあるsolrudev/SimpleInstallerのようなライブラリを試してみるとよいかもしれません。

システム上に存在するセッションの管理

PackageInstallerは、任意のアプリケーションが並行して使用している可能性もあり、複数のセッションが同一のアプリケーションを操作しようとする可能性もあります。また、現在進行形で操作されていなくても永続化されている可能性もあります。あるいは、自分のアプリケーションが同一のアプリケーションを対象とする複数のセッションを作ろうとしている可能性もあります。もしそれが望ましくない状態であれば、セッションを生成しない、あるいは既存のセッションをキャンセルする、といった処理が必要になるでしょう。そのためには、現在どのようなセッションが存在しているかを把握できる手段が必要です。

PackageInstaller.SessionとPackageInstaller.SessionInfo

ここまでの説明で出てきたPackageInstaller.Sessionは、セッションを操作するために必要なクラスでしたが、このクラスを生成する時にPackageInstalleropenSession(int sessionId)メソッドを使用したことを思い出してください。このクラスで実体化していないセッション情報がシステム上に眠っている可能性があります。それらの情報は、PackageInstallergetSessionInfo(int sessionId)というメソッドを使うと、PackageInstaller.SessionInfoというクラスのインスタンスとして取得できます。基本的にはそのアプリケーション自身が生成したセッションの情報のみが取得できます(「どのセッションの情報なら取得できるか」については後述します)。

このクラスで取得できる情報には、パッケージ名やラベルなど対象アプリケーションの詳細、出自となるPackageInstallerの詳細、セッション自体の詳細(生成時刻や最終更新時刻など)、次に説明するセッションの状態や以降で説明する追加機能のための詳細などが含まれています。

セッションの状態

PackageInstaller.SessionInfoには、そのセッションの状態をあらわすプロパティがいくつか存在します。

  • isStaged()は、そのセッションがcommit()を呼ばれて準備完了状態になっている(ユーザーからの承認待ちなどの状態にある)ことを示します
  • isActive()は、そのセッションがどこかのPackageInstallerによってopenSession()で操作されているアクティブな状態にあることを示します

PackageInstaller.Sessionclose()が呼ばれると、そのセッションは非アクティブ状態となりますが、削除されるわけではなく、同じsessionIdを使えばまた操作を再開できます。閉じた後、ユーザーのインストール承認が得られなければ、セッションは破棄されます。

セッションの自動破棄

PackageInstallerが生成するセッションは、PackageInstaller.Sessionclose()が呼ばれると永続化されて眠り続けますが、この状態でいつまでも残り続けているとやがてシステムがゴミだらけになってしまいますし、特にデバッグ中のものや実験的に動かしたアプリケーションなど、PackageInstallerを操作したアプリケーションそのものがすでに消されている可能性もあります。

そのようなセッションが残り続けないよう、システムのインストーラー サービスでは定期的に古くなったセッションをパージします。PackageInstaller.createSession()のドキュメント上では「通常は一日単位で」といった表記になっていますが、実際にPackageInstallerを操作しながらlogcatを見ていると、より頻繁に所有者がいなくなったセッションが消されていく様子が見られます。

この操作は開発者のアプリケーションからは制御できないので、セッションの有効期限は「合理的な範囲で短いもの」と考えておいたほうがよいでしょう。逆に、インストーラーのバグなどで、中途半端な状態で残ったセッションについては、あまり気にしなくても大丈夫そうです。

mySessions、allSessions、stagedSessions

PackageInstallerでは、システム上に存在しているセッションのリストをPackageInstaller.SessionInfoのリストとして取得できます。

mySessionsgetMySessions()メソッド)を使うと、現在のアプリケーションが生成した全セッションを取得できます。また、allSessionsgetAllSessions()メソッド)を使うと、システム上に存在する全セッションを取得できますが、他のアプリケーションが操作しているセッションの情報はセンシティブ情報であり、その詳細情報にアクセスするためには、自身がそのセッションのオーナーであること、AndroidManifest.xml<queries>を指定していること、あるいはandroid.permission.QUERY_ALL_PACKAGESのパーミッションを有していること…といった条件が成立している必要があります。

他にもstagedSessionsgetStagedSessions()メソッド)やactiveStagedSessionsgetActiveStagedSessions()メソッド)といったリストも取得できますが、これらも他のアプリケーションのセッション情報を含みうるので、同様の条件が求められます。

なお、セッションのオーナー(アプリケーション)は、PackageInstaller.Sessiontransfer(String packageName)メソッドを使用して、他のアプリケーションに移譲することも可能です。

アプリケーションストアを構築するための機能

PackageInstallerには、アプリケーションストアらしい機能を実現するために存在すべきAPIが含まれており、これは冒頭でも言及したとおり、本書執筆時点で最新版であるAndroid 14のベータ版でも継続的に追加されています。

複数パッケージの一括インストール(Android 10)

API Level 29(Android 10)では、PackageInstallerの1つのセッションで複数のパッケージをまとめてインストール、あるいはまとめてインストールのキャンセルができるようになりました。PackageInstaller.Sessionに追加されたaddChildSessionId()メソッドやremoveChildSessionId()メソッドで子セッションのリストを管理し、commit()でインストールを実行できるようになります。同様に、abandon()を呼び出すと子セッションも全て破棄されます。

インストール操作の事前承認(Android 14)

Android 14では、PackageInstallerに新しくrequestUserPreapproval()というメソッドが追加されています。これはインストーラーがユーザーからAPKのインストール処理について事前に承認を求めるという仕組みです。もう少し処理フローに沿っていえば、APKの情報の設定やストリームの準備がまだ整っていない段階で承認を求める、ということです。

public class PackageInstaller {
    ...
    public void requestUserPreapproval(PackageInstaller.PreapprovalDetails details,
        IntentSender statusReceiver) {
        ...

最初の引数であるPackageInstaller.PreapprovalDetailsというのは、事前承認インストールで使用される詳細設定クラスで、PackageInstaller.PreapprovalDetails.Builderクラスを使用して生成します。いくつか項目がありますが、パッケージ名、ラベル、ロケールの3つはAndroid 14 beta1時点での指定必須項目です。実際にAPKをインストールするときに、これらの項目内容が事前承認時と一致しなければ、インストールはシステムによって拒否されます。2番目の引数であるIntentSenderの使い方は、PackageInstaller.Sessioncommit()の引数とほぼ同様です。

ここで、事前「ではない」仕組みがどうなっていたかを振り返ってみましょう。インストールのユーザー承認は、PackageInstaller.Sessioncommit()によって、システムのインストーラー サービスへのIntent発行として開始され、その結果がPackageInstaller.STATUS_PENDING_USER_ACTIONであればインストール承認UIのIntent発行が必要になり、その結果がUIとしてユーザーに提示される、という流れになります。

素朴に考えるなら、この仕組みでは、事前承認は不可能です。ユーザーの承認を求めるUIはシステムのインストーラー サービスから呼び出されて表示されるもので、その操作結果はシステムのインストーラー サービスに返されます。ここにインストーラー開発者のコードが介入する余地はなく、したがって、事前承認を得るために、インストーラー サービスに対する事前承認専用のactionがIntentで送られていると考えるのが自然です。ということは、この事前承認の仕組みはおそらくAndroid 13以前にバックポートされても正常に動作しない可能性が高いのではないか、と筆者は推測します。

update ownership: パッケージ管理権の保持(Android 14)

システムでアンインストールが抑止されていないアプリケーションは、任意のインストーラーによって上書きされる可能性がありますが、他のアプリケーションストアから「いつの間にかアップデートされていた」となってしまうと、ユーザーが期待している体験が損なわれる可能性があります。アプリケーションやアプリケーションストアによっては、所定のインストーラーから整合的にアップデートされることが望ましい状況があり得ます。

これを出来る範囲で実現しようというのがAndroid 14から追加されたupdate ownershipのコンセプトです。インストールしたパッケージがそのインストーラーの管理下にあるという情報を設定し、他のインストーラーがこれを更新するためにはユーザーの明示的な同意が必要になる、という仕組みになっています。この挙動はandroid.permission.INSTALL_PACKAGESのパーミッションをもつシステムのインストーラーでも覆りません。

アプリケーションストアがシンプルに「特定のアプリケーションストアでのみ管理できる」というポリシーを施行できれば、PackageInstalerのAPIもそのように構成されたかもしれませんが、ユーザーがアプリケーションをアンインストールしてその後別のストアからインストールしてしまえば、これは簡単に覆されてしまうので、今あるアプリケーションの管理権を固定するというレベルで、望まれない上書きを抑止していると考えられます。

セッションを生成するときに、PackageInstaller.SessionParamssetRequestUpdateOwnership()メソッドを使うと、この管理権を調整できます。

InstallConstraints: ユーザー操作を中断しないアップデートを行う(Android 14)

アプリケーションのインストーラーは、アプリケーションが実行中であればそれを中断して更新を実行することになりますが、現在使用中のアプリケーションを中断するというのはあまり礼儀正しいやり方ではありません。Android 14では、更新準備が整ったセッションの実際の更新処理を、特定の条件下では先送りするInstallConstraintsという仕組みが用意されています。

先送りの設定を保持するのはPackageInstaller.InstallConstraintsというクラスで、これを生成するのはPackageInstaller.InstallConstraints.Builderの仕事です。Android 14ではPackageInstaller.InstallConstraintsに次のようなメンバーが用意されていますが、beta1の時点で何もドキュメントに説明が無いので、メソッド名から雰囲気で察しましょう。

  • isAppNotForegroundRequired()
  • isAppNotInteractingRequired()
  • isAppNotTopVisibleRequired()
  • isDeviceIdleRequired()
  • isNotInCallRequired()

Android SDKとしては、これらを個別に設定するより、PackageInstaller.InstallConstraints.GENTLE_UPDATEという定数を使用することを推奨しています。今後のAndroidのバージョンアップで、どのような場合にインストールを先送りすべきかを決める追加条件が出てくるかもしれないためです。

さて、このInstallConstraintsの使い方ですが、PackageInstallerにはこれを処理するメソッドがいくつか用意されています。

  • checkInstallConstraints() - チェックした結果をPackageInstaller.InstallConstraintsResultというクラスのインスタンスにして、コールバックとして指定されたjava.util.function.Consumeに渡し、そこからさらなる処理をインストーラーに任せます
  • commitSessionAfterInstallConstraintsAreMet() - commit()を呼び出す代わりにこれを呼び出すことで、InstallConstraintsが課す条件をクリアするまで待機し、その後で自動的にcommit()が呼び出されることになります
  • waitForInstallConstraints() - InstallConstraintsが課す条件をクリアするまで待機し、その後コールバックとして指定されたIntentSenderが呼び出されます

一番シンプルなのはcommitSessionAfterInstallConstraintsAreMet()に丸投げするアプローチですが、より細かく制御したい場合は他のアプローチを使うことになるでしょう。ちなみに前出の事前承認が無い状態でcommit()すると、InstallConstraintsの条件をクリアした時点でユーザー承認ダイアログがポップアップされることになり、おそらく体験が悪いので(予期しないタイミングで画面に出てくるとキャンセルされる可能性も大きいでしょう)、事前承認と組み合わせて使用したほうがよいでしょう。

考察とまとめ

Android 14で追加されてきた新機能も含めて、PackageInstallerの機能の大部分を説明してきましたが、これで独自のアプリケーションストアを問題なく構築できるようになったと考えるべきでしょうか?

筆者の私見としては、独自のアプリケーションストアとGoogle Play Storeとの間で生じてきた潜在的な衝突の問題などは、インストール管理権の問題などを含めいくつか解決されそうではあるものの、android.permission.INSTALL_PACKAGEをもたないアプリケーションストアの構築は、まだまだ改善の余地がありそうに思えます。たとえば、ユーザー承認を伴わないアプリケーションの自動更新は、現状ではまだ実現できないでしょう。PackageInstallerrequestUserPreapproval()メソッドは、何も考えずに名前だけを見ると、自動更新機能を実現するためのものにも見えますが、実際にはユーザー操作による承認を伴うものであり、任意のアプリケーションストアで自動更新を実現できるべきであるとしたら(ここには議論の余地があるでしょう)、それは未来形の話となりそうです。

最後になりますが、このPackageInstallerの機能を利用して、GitHub ActionsのビルドアーティファクトやGitHub Releaseの登録ファイルをパッケージのソースとするインストーラーを実現するライブラリを、atsushieno/android-ci-package-installerというGitHubリポジトリで公開しています。この章で紹介したAndroid 14の新機能も一部で使われているので、コードの具体例を見たい読者は実用例として参考にしてください。

脚注
  1. まあそうは言ってもatsushienoはそれなりに訓練された原稿書きになってしまったので、日頃のほかの文章と比べるとだいぶTechBooster寄りのテイストであろうと思います ↩︎

  2. デバイスのベンダーはandroid.permission.INSTALL_PACKAGESというパーミッションをもつアプリケーションをプリインストールできるので、インストーラーのパーミッションに関する問題の大部分は無関係です ↩︎

  3. iOS17は本書執筆時点で未公開です ↩︎

  4. たとえばAndroid 14の"Features and APIs Overview"のドキュメントはこの機能に関して "If you develop a third-party app store, we would love to hear your feedback" と注記しており、またAndroidの最新リリースが出るたびにその内容を分析しているCommonsWareでは"What Might Be Signs of Competition"という見出しでこのクラスに言及しています ↩︎

  5. 同じようなパーミッション設定の例としては、パスワード等も含めたユーザーのキー入力を盗み見できる可能性があるIME(入力メソッド)のアプリケーションにも明示的な許可が必要です ↩︎

  6. addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)を指定しないと、contextがActivityとしてUIコンテキストを有していない限りエラーになってしまいます(※2024/10/31 追記: Android 15ではこの問題が修正されていると教えていただきました https://bsky.app/profile/tnj.dev/post/3l7roe7iubj25↩︎

  7. 2023年の本書執筆時点でGoogle Play Servicesが動作する最低API Levelは19(Android 4.4)なので、Googleとしての公式なサポート対象に含まれているバージョンがカバーできていない(ギリギリアウト)とはいえます ↩︎

  8. ここでもaddFlags(Intent.FLAG_ACTIVITY_NEW_TASK)を指定しないと、contextがActivityとしてUIコンテキストを有していない限りエラーになってしまいます ↩︎

Discussion