In-App Updateを実装する

Androidでアプリ内アップデート（In-App Update）を実現できるようになってだいぶたっていますが、今更ながらやり方を解説してみようと思います。

公式にはこちら
https://developer.android.com/guide/playcore/in-app-updates

## 要件
- Android 5.0 (API Level 21)以上
- Play Core library 1.5.0以上

Android 4系以下には適用できないです。
また、当たり前ですが、実際にアップデートを行うのはPlayストアアプリなので、Playストアアプリが使えない環境では使えません。

## 2種類のアップデート方法

アップデート方法にはFlexibleとImmediateの2種類が選択可能です。

### Flexible

アップデートのダウンロード中もアプリの使用が可能です。
以下のように、ダイアログが表示され、「更新」をユーザーが選択すると、ダウンロードが始まりますが、その間もアプリの継続利用が可能です。ダウンロードが完了しても即座にアップデートは行われず、アップデート実行のタイミングを制御できます。
即座にアップデートしなければならないような、重大なアップデートではなく、アプリの状態を維持したいとか、サイズが大きくアップデートに時間がかかるアプリなどで利用するとよいと思います。
当然、アップデートの実行時にはアプリは終了しますが、アップデート完了後、自動的に再起動されます。

|||
|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/65fdb643-4b34-b5df-a610-e54e1e117efb.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/05cace0f-da92-bb67-0a1e-a12368613758.png)
|

### Immediate

アプリの使用を中断し、アップデートを実行させます。以下のように全画面で更新訴求を行います。
Flexibleと違って、「ダウンロードしない」という選択肢はありません。右上の×やバックキーで抜けることはできますが、Flexibleよりも強いアップデート訴求です。「更新」を選択すると、ダウンロードからアップデートの実行までまとめて行われます。アップデートの完了後アプリは再起動します。
Flexibleに比べて、ワンタップでアップデートから再起動まで行われるため、ユーザーの手数としてもこちらの方が小さいです。即座にアップデートして欲しい場合や、小規模なアプリでアプリの継続が不要な場合などで利用するとよいと思います。

|||
|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/79b84ca6-ff8e-c720-405c-8c6e7cee03ee.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/b288a9ca-236e-d34a-1b19-580e67f90cdf.png)|


## 準備

Play Core libraryをdependenciesに追加します

```gradle
    implementation "com.google.android.play:core:1.7.2"
    implementation "com.google.android.play:core-ktx:1.7.0"
```

当然、ktxは必須ではないです。

## アップデート有無の確認

```kotlin
val appUpdateManager = AppUpdateManagerFactory.create(applicationContext)
appUpdateManager.appUpdateInfo.addOnSuccessListener {
   // AppUpdateInfoが渡ってくる
   ...
}
```

`AppUpdateManagerFactory.create()`の引数はcontextですが、ApplicationContextを渡すそうです。
`getAppUpdateInfo()`は非同期で情報取得を行います。戻り値はTaskで、このTaskにコールバックを登録することで結果を取得します。リスナーはUIスレッドでコールされますので、直接UIの変更なども可能です。

また、coroutinesで戻り値として受け取ることもできます。

```kotlin
scope.launch {
    val info = appUpdateManager.requestAppUpdateInfo()
    ...
}
```

`requestAppUpdateInfo()` はktxで定義されているsuspend拡張関数です。
ただ、一回のコールバックですむ処理なのでcoroutinesを使うほどの処理ではないのと、内部スレッドでExceptionが発生し、アプリが落ちてしまうことがありました。ソースコードがないこともあり、原因は分かりませんでした。ここではcoroutinesは使わない方がよいかもしれません。

## Immediateアップデートを実行する

Immediateアップデートは更新実行後は勝手にアップデート＆再起動が行われるため、単純にアップデート画面を表示するだけでよいのであれば、以下のようにするだけでよいです。

```kotlin
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
    info.isImmediateUpdateAllowed) {
    val options = AppUpdateOptions.defaultOptions(AppUpdateType.IMMEDIATE)
    appUpdateManager.startUpdateFlow(info, activity, options)
}
```

アップデート可能な状態で、Immediateアップデートが可能な場合に、`startUpdateFlow`を実行します。これで前述のアップデート画面が表示されます。（アップデート可能かつImmediateもしくはFlexibleアップデートができないという状態があり得るのかはよく分かりません）`isImmediateUpdateAllowed`は`isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)`と等価なktxの拡張プロパティです。

ただし、アップデート画面はキャンセル可能なのでこれではキャンセルされても検出することができません。キャンセルされた場合に、アップデートの必要性を訴えるとか、次回の訴求を計画するなどを行いたい場合は、リクエストコード付きで実行します。

```kotlin
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
    info.isImmediateUpdateAllowed) {
    appUpdateManager.startUpdateFlowForResult(
        info, AppUpdateType.IMMEDIATE, activity, UPDATE_REQUEST_CODE
    )
}
```

結果はonActivityResultで受け取ります。Fragmentで受け取りたい場合は、第三引数がFragmentになったメソッドがktxで定義されているのでそちらを利用します。

## Flexibleアップデートを実行する

Flexibleアップデートはもう少し実装が必要になります。アップデート画面の表示はImmediateと同様です。

```kotlin
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
    info.isFlexibleUpdateAllowed) {
    appUpdateManager.startUpdateFlowForResult(
        info, AppUpdateType.FLEXIBLE, activity, UPDATE_REQUEST_CODE
    )
}
```

アップデートが開始されると、`onActivityResult`に`resultCode == RESULT_OK`で入ってきます。Flexibleではバックグラウンドでダウンロードが進行しますのでこの後の処理の実装も必要です

```kotlin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == UPDATE_REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            // アップデート実行後の処理
        }
        return
    }
    super.onActivityResult(requestCode, resultCode, data)
}
```

### アップデートの進捗を監視する

`registerListener`メソッドでlistenerを登録することでアップデートの進捗を監視することができます。

```kotlin
// アップデートの開始前に登録
appUpdateManager.registerListener(listener)

// アップデート完了後に登録解除
appUpdateManager.unregisterListener(listener)
```

リスナーではInstallStateを受け取ることができ、`installState.installStatus()`の値で状態を調べることができます。最低限の実装をするなら、DOWNLOADEDになった時点でアップデート実行のボタンを表示するぐらいでしょう。ボタンを表示しないでいきなりアップデートを開始することもできるわけですが、その場合はFlexibleアップデートを行う意味があまりないでしょう。
状態がDOWNLOADINGになれば`totalBytesToDownload()`でダウンロードするバイト数を取得することができます。`bytesDownloaded()`で、ダウロード済みのバイト数を取得できるので、両者からプログレス表示もできるとは思いますが、私が試した範囲では`totalBytesToDownload()`はそれらしい値が返ってきましたが、`bytesDownloaded()`は0のまま変化することなく、DOWNLOADEDになってしまいました。試したアプリはダウンロードサイズが3MB未満だったので、小さすぎるととれないのか、十分に大きなアプリでもとれないのかなどはよく分かりませんでした。

```kotlin
override fun onStateUpdate(state: InstallState) {
    when (state.installStatus()) {
        InstallStatus.PENDING -> {
        }
        InstallStatus.DOWNLOADING -> {
        }
        InstallStatus.DOWNLOADED -> {
        }
        InstallStatus.INSTALLING -> {
        }
        InstallStatus.INSTALLED -> {
        }
        InstallStatus.CANCELED -> {
        }
        InstallStatus.FAILED -> {
        }
        InstallStatus.UNKNOWN -> {
        }
    }
}
```

状態がDOWNLOADEDになればアップデートの実行が可能です。
何らかのUIを表示して、ユーザーに実行してもらうのがよいでしょう。
GoogleのサンプルではSnackbarを使っています。
`completeUpdate()`を実行するとアップデートが実行され、アプリは再起動します。

```kotlin
button.setOnClickListener {
     appUpdateManager.completeUpdate()
}
```

### ダウンロード済みの対応

ダウンロードを開始して、完了前にアプリを閉じられてしまう場合もあるでしょう。その場合、次の起動時にダウンロード完了状態になっている場合があり、ダウンロード開始をスキップして、アップデートボタンを表示する処理が必要になります。
初回の、AppUpdateInfoを取得したところで、以下のように確認します。

```kotlin
if (info.installStatus == InstallStatus.DOWNLOADED) {
    showUpdateButton()
}
```

## アップデート画面表示の条件

ここまでの説明では、アップデート可能であると分かったら即座にアップデートという実装ですが、ちょっとそれでは過剰だということも多いと思います。そのさじ加減をするために使える情報がAppUpdateInfoにあります。

`clientVersionStalenessDays()`によってアップデートを検出してからの日数を取得できます。アップデートが公開されてから、ではなく、ストアアプリがアップデートがあることを検出してからです。しばらくの間は自主的なアップデートに任せ、一定期間ユーザーがアップデートしなかった場合に初めて発動するという実装が可能です。戻り値はIntegerであり、アップデートがない場合などではnullが返るため、nullチェックが必要です。
また、アップデート実行後、新バージョンをアンインストールし、旧バージョンをインストールした場合、この日付情報が削除されるのかアップデートはあるのに、stalenessの値がnullとなってしまうようです。他にnullになる条件があるかは分かりませんが、アップデート可能な状態でもnullである可能性はあると考えておきましょう。

```kotlin
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
    info.clientVersionStalenessDays().let { it != null && it >= DAYS_FOR_UPDATE })
```

`updatePriority()`で、アップデートのプライオリティ（0～5）を取得することができます。公開するAPKに対して設定することができ、緊急度に応じた値を設定することで、アップデート画面表示までの日数や頻度、ImmediateかFlexibleかの変更などをこの値を元に分岐させておくとより柔軟な対応ができるでしょう。ただ、この値はGoogle Play developer APIでのみ設定可能で、現時点ではPlay Consoleからは設定できないようです。

`availableVersionCode()`で、アップデート可能なAPKのバージョンコードを取得することが来ます。バージョンコードをただの連番としている場合はあまり意味のある情報になりませんが、桁ごとにmajor/minor/patchといったバージョン構造を分けている場合は、それがmajorバージョンアップなのか、minorバージョンアップなのかを調べることができますので、その情報に基づいて処理を分けてもよいでしょう。

## 動作確認

ストア公開版と同じパッケージ名かつ同じ署名をつけたAPKかつ、ストア版よりもVersionCodeが低いAPKを作れば、ストアからダウンロードしなくてもアップデート機能の検証が可能です。
リリース署名ができない環境の場合は、内部テストなどを利用する必要があるでしょう。

## まとめ

これまで、アップデート訴求は各アプリが独自で実装し、バージョンの情報も独自の方法で配信する必要がありましたが、やっとPlayストアの機能を使って実装できるようになりました。アップデートの実行までをアプリで制御できるので、この方法を利用しない手はないでしょう。