Google I/O 2019で発表され正式公開されたin-app updates APIであるAppUpdateManagerを利用した、アプリ内でのアップデートチェックとアップデート対応方法について解説します。
また、初心者がハマるであろうポイントについてもお伝えしますが、in-app updates APIはGoogle Play ストア アプリと連携するためのAPIと考えた方が良いです。アップデート有無の判定自体はGoogle Play ストア アプリが行っています。そのため、Google Playストアアプリがアプリの更新を認識してくれないと、in-app updates apiは更新ありと判定してくれません。
2020/8/2追記
最新のcom.google.android.play:core:1.8.0
では少し仕様変更になっているようで、追って更新します。
はじめに
API Level
AppUpdateManagerは Android 5.0 (API level 21) 以上でのみサポートされています。
公式ドキュメント
公式なドキュメントのリンクを記載しておきます。
アップデートの種類
FlexibleとImmediateの2種類のフローが用意されています。デフォルトはどちらも利用可能で、アプリ開発者が好きな方でアップデート通知するように実装出来ます。
Flexibleフロー
Flexibleフローは更新を検出したらダイアログ形式で通知し、ユーザが更新ボタンを押したらバックグラウンドでダウンロードするフローです。ダウンロード中は通常通り、アプリを利用できます。
ダウンロード完了後は、ユーザに何らかの方法で通知して(公式サイトのサンプルだとSnackbar)、インストール作業に移行します。インストール時は全画面表示になり、インストール完了後に自動でアプリが再起動します。
Immediateフロー
Immediateフローは更新検出後、全画面表示でアップデートを促します。ただし、右上の×ボタンや戻るボタンで簡単に消せるため、強制アップデートとまでは言えません。
ユーザが更新ボタンを押すと、そのままダウンロードおよびインストール作業に移行し、通常のアプリはバックグラウンドに移行します。インストール作業移行のフローはFlexibleと同じです。
実装
githubにサンプルコードを用意していますので、併せて参照して下さい。
ライブラリ
PlayCoreのライブラリをapp/build.gradleに追加します。2019/10/27時点の最新バージョンは1.6.4です。各バージョンの詳細はリリースノートを参照して下さい。
implementation 'com.google.android.play:core:1.6.4'
UpdateFlowステータスリスナーの登録と処理
InstallStateUpdatedListenerは、startUpdateFlowForResult後のイベントのコールバックイベントリスナーです。Flexibleフローの場合に、アップデートイメージのダウンロード完了のイベントをトリガーにして、ユーザに通知してインストール作業に移行させるために利用します。一方、インストール完了イベントトリガーでリスナーを解除します。
installStateUpdatedListener = InstallStateUpdatedListener { installState ->
when (installState.installStatus()) {
InstallStatus.DOWNLOADED -> {
Log.d(TAG, "Downloaded")
updaterDownloadCompleted()
}
InstallStatus.INSTALLED -> {
Log.d(TAG, "Installed")
appUpdateManager.unregisterListener(installStateUpdatedListener)
}
else -> {
Log.d(TAG, "installStatus = " + installState.installStatus())
}
}
}
appUpdateManager.registerListener(installStateUpdatedListener)
アップデート検知方法
肝心のアップデート検出については、appUpdateInfoTask.addOnSuccessListenerの部分が該当します。updateAvailabilityでUpdateAvailability.UPDATE_AVAILABLEが帰ってきた時にのみ、in-app apiでアップデート可能です。また、isUpdateTypeAllowedの部分で、許可されているアップデートタイプを確認します。ここはデフォルトでAppUpdateType.FLEXIBLE, AppUpdateType.IMMEDIATEの両方がtrueになるはずですので、自分が利用したいフローの方で先に進むように実装すると良いと思います。
そして、startUpdateFlowForResultの部分でアップデート作業に移行します。ここから先、FlexibleフローかImmediateフローかでUI表示等含めてフローが変わってきます。
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener(playServiceExecutor, OnSuccessListener { appUpdateInfo ->
when (appUpdateInfo.updateAvailability()) {
UpdateAvailability.UPDATE_AVAILABLE -> {
val updateTypes = arrayOf(AppUpdateType.FLEXIBLE, AppUpdateType.IMMEDIATE)
run loop@{
updateTypes.forEach { type ->
if (appUpdateInfo.isUpdateTypeAllowed(type)) {
appUpdateManager.startUpdateFlowForResult(appUpdateInfo, type, this, REQUEST_UPDATE_CODE)
return@loop
}
}
}
}
else -> {
Log.d(TAG, "updateAvailability = " + appUpdateInfo.updateAvailability())
}
}
})
エラー発生時の処理
startUpdateFlowForResultに対するonActivityResultの処理の部分はあまり大した事がなく、エラーが起きた時に再度startUpdateFlowForResultを発行するためのもの程度で捉えて頂いて大丈夫です。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
if (requestCode == REQUEST_UPDATE_CODE) {
if (resultCode != RESULT_OK) {
// If the update is cancelled or fails, you can request to start the update again.
Log.e(TAG, "Update flow failed! Result code: $resultCode")
}
}
}
ダウンロート完了とインストール開始通知(Flexibleフローのみ)
Flexibleフローの場合、ダウンロード完了のイベントをトリガーにユーザにインストール作業に移行を促します。サンプルコードはSnackbarで通知した例ですが、appUpdateManager.completeUpdate()の部分を実行することで、勝手にインストール作業に移行して、アプリが再起動します。
private fun updaterDownloadCompleted() {
Snackbar.make(
findViewById(R.id.activity_main_layout),
"An update has just been downloaded.",
Snackbar.LENGTH_INDEFINITE
).apply {
setAction("RESTART") { appUpdateManager.completeUpdate() }
show()
}
}
アプリ再開時のケア
アプリがバックグラウンドに移り、再度アプリがフォアグランドに来たときのケアが必要にあります。Flexibleフローの場合は、ダウンロード完了状態であれば再度インストール促し表示に移ります。Immediateフローの場合には、全画面表示をさせます。
override fun onResume() {
super.onResume()
appUpdateManager.appUpdateInfo.addOnSuccessListener(playServiceExecutor, OnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
// If the update is downloaded but not installed,
// notify the user to complete the update.
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED)
updaterDownloadCompleted()
} else {
// for AppUpdateType.IMMEDIATE only
// already executing updater
if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.IMMEDIATE,
this,
REQUEST_UPDATE_CODE
)
}
}
})
}
ハマるポイント
updateAvailabilityがUpdateAvailability.UPDATE_NOT_AVILABLE(1)しか来ない
ガイドライン通りに実装後のデバッグ時に想定外にはまりました。Google Playストアにアップしている最新版のバージョンコードを取得して、現在プロジェクトのバージョンコードと比較して現在のコードが古ければアップデート検出してくれるよね?って思っていましたが、実はそうではありません。
そのため、いくら動かしてもupdateAvailabilityがUpdateAvailability.UPDATE_NOT_AVILABLE(1)しか返して来ず、先に進めないのです。理由は簡単で、appUpdateInfo.availableVersionCodeが常に0でストア側が古いと認識しているためです。
同じような事象で悩んだ人が複数居る様子です。
- Android In App Updates - Not able to detect the update in AppUpdateInfo
- Android In-App update API return 0 as availableVersionCode & updateAvailability as 1 (UPDATE_NOT_AVAILABLE)
実はこれは正しい動きで、前述の通りアップデート検知処理自体はGoogle Playストアアプリが行っているためです。そのため、リリースする前にデバッグするには後述のテスト専用のFakeAppUpdateManagerを利用する必要があります。
Google Playストアアプリが更新を認識しているけどUPDATE_NOT_AVILABLEが来る
アプリ説明ページやベータ版のタブで更新と認識しているだけでは駄目な様子です。以下の様に、アップデートタブにリストアップされる必要があります。Playストアアプリのキャッシュ更新アルゴリズムは不明です。
どうしても駄目な場合の最終手段
Google Playストアアプリのキャッシュ更新(アップデート検出サイクルなど)が不明のため、手っ取り早く更新を認識してもらうには以下の方法があります。Playストアアプリを完全に終了させる方法で私の場合はほぼ更新認識されるようになりました。
- Google Playストアアプリを完全に終了させて(Kill)、Playストアアプリを再起動
- Google Playストアアプリのデータ&キャシュ削除
テストコード
テスト用のサンプルコードを用意しておきました。
ポイントは以下の2点です。
- FakeAppUpdateManagerはUnitTest用のためUI表示が一切されない
- そのため、自分でAPIを叩いて先に進める必要がある
- アップデートタイプやバージョンコードは自由に設定出来る
こんな感じでテスト出来ます。
@Test
fun test_FlexibleUpdateSuccess() {
fakeAppUpdateManager.partiallyAllowedUpdateType = AppUpdateType.FLEXIBLE
fakeAppUpdateManager.setUpdateAvailable(10)
ActivityScenario.launch(MainActivity::class.java)
assertTrue(fakeAppUpdateManager.isConfirmationDialogVisible)
fakeAppUpdateManager.userAcceptsUpdate()
fakeAppUpdateManager.downloadStarts()
fakeAppUpdateManager.downloadCompletes()
Espresso.onView(
allOf(
isDescendantOfA(instanceOf(Snackbar.SnackbarLayout::class.java)),
instanceOf(AppCompatButton::class.java)
)
).perform(ViewActions.click())
assertTrue(fakeAppUpdateManager.isInstallSplashScreenVisible)
fakeAppUpdateManager.installCompletes()
}
Google Playストアアプリのデータ削除
FakeAppUpdateManagerを使った場合の注意点ですが、setUpdateAvailableのAPIで設定したバージョンコードはしばらく残り続けます。そのため、前述のGoogle Playストアアプリのデータ削除(ストレージを消去)により、設定したバージョンコードを削除(0になる)しておいた方が無難です。
最後に
公式ドキュメントの情報不足でかなりはまりましたが、FirebaseのRemote Configを利用したアップデート検出&促しに比べて、一度実装するとメンテナンスフリーで対応出来るため楽で良いと思います。
Google Playストアアプリがインストールされていない特に中国とかの環境だとどうなるの??
間違い等あればご指摘頂けますと助かります。