Posted at

Android in-app Updates API 解説と雑感


in-app Updates APIとは

2018年のAndroid Dev Summitで発表された技術で、Play Storeにアプリの新しいバージョンが存在する場合、アプリ上でそれを知らせてなおかつその場でアップデートすることができます。

発表されてからしばらくはβテスト状態だったようですが、Google I/O 2019にて正式版がリリースされました。

とりあえずどんなものかを試してみたので、簡単な解説も含めて紹介したいと思います。

公式のドキュメントについてはこちらを参照してください。今回はこちらに記述されているサンプルコードを基に解説を行います。

https://developer.android.com/guide/app-bundle/in-app-updates


必要条件

in-app Updates APIを使うための条件は以下のとおりです。


アップデート通知UIのタイプ

in-app Updates APIでは、2種類のタイプのUIが用意されています。


Immediate

アップデート通知は全画面で表示されます。拒否する手段がないため、アップデートしないとアプリが使えないため、致命的なバグがある場合やAPIのレスポンスに変更がある場合など、必ずアップデートしてほしいときに使うとよいでしょう。


Flexible

アップデート通知はダイアログで表示されます。アプリの更新はバックグラウンドで行い、更新中もユーザーはアプリを使うことができます。またユーザーはアップデートを拒否することもできます。

Immediate
Flexible

device-2019-05-08-150926.png
device-2019-05-08-151135.png


実装してみる

では実際にアプリに組み込んでみましょう。

アプリ起動時のみチェックしたいときは起動Activityで、すべてのActivityでチェックしたいのであればBaseActivity的なクラスを用意して実装するとよいでしょう。


Immediate(全画面)タイプ

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

manager = AppUpdateManagerFactory.create(this)
manager.appUpdateInfo.addOnCompleteListener { task ->
val info = task.result

when (info.updateAvailability()) {
UpdateAvailability.UPDATE_AVAILABLE -> {
// 更新処理を行う
manager.startUpdateFlowForResult(info, AppUpdateType.IMMEDIATE, this, REQUEST_CODE)
}
UpdateAvailability.UPDATE_NOT_AVAILABLE -> {
// アップデートがない場合の処理。そのままアプリを起動するなど
}
}
}
}

まずAppUpdateManager.getAppUpdateInfo()でアプリのアップデート情報(AppUpdateInfo)を取得するためのタスクを作成し、OnCompleteListenerを設定します。

Listenerの中で取得したタスク結果から、AppUpdateInfoを取得し、AppUpdateInfo.updateAvailability()でアップデートが存在するかどうかを確認します。型はintですが、UpdateAvailabilityに次の定数が設定されています。


  • 0: UNKNOWN

  • 1: UPDATE_NOT_AVAILABLE

  • 2: UPDATE_AVAILABLE

  • 3: DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS

0〜2については定数名のとおりですが、3が少し分かりづらいと思います。これについては後述します。

さて、アップデートが存在することがわかったら、実際に更新処理を行います。AppUpdateManager.startUpdateFlowForResult()を実行すると、更新するための画面が自動で開きます。

あとは放っておけばアプリが更新されるのですが、更新中にアプリがバックグラウンドに行ってしまった場合、復帰時に更新処理を継続したいですよね。

そこで登場するのが、先ほど出てきたDEVELOPER_TRIGGERED_UPDATE_IN_PROGRESSです。これはこれは現在更新処理中ということを表すフラグです。AppUpdateInfo.updateAvailability()の値がこうなった場合、再度AppUpdateManager.startUpdateFlowForResult()を実行することで処理が再開されます。

override fun onResume() {

super.onResume()

manager.appUpdateInfo.addOnCompleteListener{task ->
val appUpdateInfo = task.result

if(appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
// 既に更新処理が走っている場合、更新をresumeする
manager.startUpdateFlowForResult(appUpdateInfo, AppUpdateType.IMMEDIATE, this, REQUEST_CODE)
}
}
}


Flexible(ダイアログ)タイプ

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

manager = AppUpdateManagerFactory.create(this)

// アップデートのダウンロード状態の変化時に動くListener
listener = InstallStateUpdatedListener {
if (it.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate()

// メモリリーク対策のため、Listenerの登録を解除する
manager.unregisterListener(listener)
}
}
manager.registerListener(listener)

manager.appUpdateInfo.addOnCompleteListener { task ->
val info = task.result

when (info.updateAvailability()) {
UpdateAvailability.UPDATE_AVAILABLE -> {
// 更新処理を行う
manager.startUpdateFlowForResult(info, AppUpdateType.FLEXIBLE, this, REQUEST_CODE)
}
UpdateAvailability.UPDATE_NOT_AVAILABLE -> {
// アップデートがない場合の処理。そのままアプリを起動するなど
}
}
}
}

private fun popupSnackbarForCompleteUpdate() {
// 更新完了のSnackbarを表示する
Snackbar.make(findViewById(R.id.activity_chooser_view_content),
"An update has just been downloaded.", Snackbar.LENGTH_INDEFINITE)
.setAction("UPDATE") { manager.completeUpdate() }
.show()
}

アップデートチェックの部分はほぼImmutableと同じです。AppUpdateTypeがFLEXIBLEになっているのが違いです。

Flexibleタイプでは、更新するアプリのダウンロード中もユーザーはアプリを使い続けることができるため、ダウンロードが終わったことをユーザーに知らせる必要があります。

そこで、AppUpdateManagerInstallStateUpdatedListenerを登録します。Lisnenerの中でアプリの更新状態のステータスを監視し、ダウンロードが完了したらその旨をユーザーに通知します。サンプルコードではSnackbarを表示しています。

その後、AppUpdateManager.completeUpdate()を実行することでアプリのアップデートが実行されます。

Immutableタイプと同様に、アプリがバックグラウンドに行ってしまった場合の復帰処理を入れておけばより安全になるでしょう。

サンプルコードでは、既にアプリがダウンロード済みだった場合にSnackbarを表示するようにしています。

override fun onResume() {

super.onResume()

manager.appUpdateInfo.addOnCompleteListener { task ->
val appUpdateInfo = task.result

// アップデートファイルがダウンロード済であればSnackbarを表示する
if(appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
popupSnackbarForCompleteUpdate()
}
}
}


ImmediateとFlexibleを使い分けたい場合

現在、in-app Update APIには、ImmediateとFlexibleを使い分けるための便利な機能はないようです。

例えば、「古すぎるバージョンのアプリを使ってる人は必ず更新してほしいけど、比較的新しいバージョンを使ってる人はアップデートを通知するだけにしたい」といった場合、ImmediateとFlexibleを使い分けたくなってきます。

こういう場合、例えばしきい値となるバージョンコードをFirebase Remote Configを使って設定しておけば、「このバージョンから下だったらImmediate、上だったらFlexible」といった具合に、アップデートを開発者側で自由にコントロールできるのではないかと思われます。


注意点


公開しているアプリと同じパッケージ名でテストする

ストアにアップロードされているアプリと同じパッケージ名でないと、アップデートのチェックが正常に行なえません。例えばデバッグビルド時にパッケージ名に.debugのようなsuffixをつけている場合はアップデート情報を取得できません。

アプリの署名鍵については、ストアにあるものと異なっていてもアップデートが存在するかどうかのチェックは行なえますが、実際にアプリを更新することはできません。


App Bundleを使っている場合の容量制限

私は確認していないのですが、ドキュメントによると、App Bundle使用時にin-app Updates APIでダウンロードできるファイルサイズは上限150MBとのことです。そこまで大きな更新をするということはあまりないとは思いますが、一応覚えておいたほうが良さそうです。


雑感

アプリの更新チェックとアップデートを同時にやってくれるので、自前でこの辺の処理を作らなくてすむというのははありがたい機能です。

今回実際に動作確認の意味も含めて実装してみましたが、若干ドキュメントが説明足らずな部分があるため戸惑ったものの、作ってしまえばかなりシンプルな実装になりました。現行のアプリに追加で実装するのも面倒ではなさそうです。