Google Playのin-app updatesを使って最高のアップデート体験を実現する

こんにちは。Androidエンジニアのsyarihuです。

私が開発を担当しているサービス「マネーフォワード ME」では、日々たくさんのアップデートを行っています。新しい機能の提供であったり、不具合の修正など、アップデートの内容はさまざまです。

たとえばAndroid版「マネーフォワード ME」の場合は、Playストアからアプリを配信しています。アプリのアップデートを受け取るためにはPlayストアからの自動アップデートを待つ、あるいは自動アップデートを有効にしていない場合はユーザーが自らアプリのアップデートがあるかを確認し、アップデートがある場合は明示的にアップデートボタンを押すことでアプリのアップデートをユーザーのAndroid端末に反映します。

新機能の提供や致命的な不具合の修正などのアップデートはアプリの提供者からするとなるべく早めにアップデートをしてほしいところですが、すべてのユーザーがアップデートをしてくれるとは限りません。
そもそもアップデートに気づいていなかったり、Playストアにいってアップデートボタンを押してアップデートが完了するのを待つのが手間でアップデートをする気が起きないなど、アップデートをしないのはユーザーによってさまざまな理由が考えられます。

そこで、アップデートに気づけない問題やPlayストアアプリに遷移してアップデートをしなければならないなどの手間を解決するための手段として、Googleが公式で提供するアプリ内でアプリのアップデートを提供できるin-app updatesという仕組みがあります。
in-app updatesを利用することで、アプリ内でアップデートを検知してユーザーにアップデートを促したり、バックグラウンドでアップデートのダウンロードを行うなど、柔軟にアップデートを提供できます。

本記事では、in-app updatesについての概要やAndroid版「マネーフォワード ME」でのアプリ内アップデートの利用例、AACを使ったin-app updatesのサンプル実装、そしてInternal app sharingを利用したin-app updatesの検証方法について解説します。

in-app updatesとは

in-app updatesとは、Googleが公式に提供するアプリ内アップデートの仕組みです。
Play Core Library 1.5.0以上を導入することで利用できます。最低動作バージョンはAPI Level 21以上のため、Android 5.0以上のデバイスで利用できます。

in-app updatesにはFlexibleとImmediateの2種類のアップデート方式があります。Flexibleはバックグラウンドでダウンロードを行い、ユーザーが任意のタイミングでアップデートを反映する方式です。ダウンロード中は引き続きアプリの使用ができます。Immediateは全画面でアップデート画面を表示してアップデートが終了したら即時反映する方式です。

それぞれのアップデート方式について詳しく見ていきましょう。

Flexibleアップデート

Flexibleアップデートは、バックグラウンドでダウンロードやインストール、インストール状態の監視などを行うアップデート方式です。ダウンロード中は引き続きアプリの使用ができます。

▼ Flexibleアップデートの例(in-app updatesの公式ドキュメントより引用)

アプリはユーザーに対してアップデートがあることを通知します。ユーザーが通知からアップデートを行うことを承認すると、Google Playがバックグラウンドでアップデートデータのダウンロードを行います。
アップデートデータのダウンロードを行っている間はユーザーはアプリを使い続けられます。
ダウンロードが終わるとアプリはユーザーに対してアップデートを行う準備ができたことを通知します。ユーザーが通知からアップデートの反映をリクエストすると、Google Playがアップデートを反映して自動的にアプリを再起動します。

ユーザーの任意のタイミングでアップデートを提供できる仕組みのため、これは重要ではないアップデートを提供する場合に適しています。

Immediateアップデート

Immediateアップデートは、ユーザーに対して全画面でアップデート画面を表示するアップデート方式です。

▼ Immediateアップデートの例(in-app updatesの公式ドキュメントより引用)

Flexibleアップデートとは違い、Immediateアップデートはアップデートが終わるまではユーザーはアプリを利用することはできないため、重要なアップデートを提供する場合に適しています。
右上に閉じるボタンがついていますが、アップデート画面の表示についてはアプリがライブラリのAPIを使ってGoogle Playにアプリの更新を確認し、更新がある場合はアップデート画面を表示するような実装になるため、アップデートするまで次に進めないようにすることで強制アップデートを実現できます。

Play ConsoleへアプリのアップロードをGoogle Play Developer API経由で行う場合のみ、アプリ内アップデートのPriority(優先度)を設定できます。たとえば重要なセキュリティアップデートを行う場合には高いPriorityを設定し、高いPriorityの場合にはImmediateを使って強制アップデートを促すこともできます。

Android版「マネーフォワード ME」におけるin-app updates

Android版「マネーフォワード ME」では、インストール直後の新規ユーザーと利用中の既存ユーザーでアップデート方式を使い分けるようにしました。

Flexibleアップデート

既存ユーザーに対してはFlexibleアップデートによるアプリ内アップデートを提供しています。
どのようにアプリ内アップデートが行えるのか順番に見ていきましょう。

はじめにアップデートの通知です。アップデートにすぐに気づけるように、ホームの一番上にアップデートの通知を表示します。

通知をタップすると、in-app updatesのFlexibleアップデートの画面を表示します。

更新ボタンを押すと、アップデートのダウンロードを開始します。ダウンロード中はアプリを継続して利用できます。

アップデートのダウンロードが完了すると、ダウンロードの完了を通知します。
ユーザーが通知をタップすると、アップデートのインストールを実行します。

インストール中はGoogle Playの画面が全画面で表示されます。この間はアプリを利用できませんが、ダウンロードが既に終わっているためインストールはすぐに終わります。

インストールが終わるとアプリが自動で再起動されるので、以降は通常通りアプリを利用できます。
これを実際に動かしてみると次のようになります。

ユーザーに邪魔にならない形で通知を行い、ダウンロードからインストールまですべてアプリ内で行えるので、ストアに遷移することなくスムーズにアプリのアップデートを提供できるようになりました。

Immediateアップデート

アプリをインストールするだけして、しばらく起動していないユーザーもいます。
そういったユーザーに対してはログインする前に重要なアップデートがあれば、Immediateアップデートを使ってアップデートを促すようにしています。
こちらもどのようにアプリ内アップデートを提供しているのか順番に見てみましょう。

ユーザーが規約に同意して始めようとした場合に、アップデートを促すダイアログを表示します。

アプリを更新するボタンを押したときにin-app updatesのImmediateアップデートの画面を表示します。

右上のボタンから更新をやめることもできますが、こちらは閉じても更新するまで先に進めないようにして強制アップデートを実現しています。
実際に動かしてみると次のようになります。

こちらもPlayストアに遷移せずにアップデートを実現できるため、通常のアップデートよりはスムーズにアップデートができるようになっています。

AACを利用したin-app updatesの実装

Android Architecture Componentを利用することで、in-app updatesの実装をViewModel内にまとめて、View側ではLiveDataで状態を監視してUIに反映するだけでアプリ内アップデートを提供できるようになります。
ここでは、AACを利用してアプリ内アップデートを提供する実装例を紹介します。

Play Core LibraryとAACを導入する

in-app updatesを利用するには、Play Core Libraryの1.5.0以上が必要です。
Play Core Libraryのリリースノートからライブラリの最新バージョンを確認して依存関係に追加します(ここでは2020年8月時点での最新バージョンである1.8.0を導入します)。

dependencies {
    implementation "com.google.android.play:core:1.8.0"
}

AACのLiveDataとViewModelを利用するため、lifecycle-livedataとlifecycle-viewmodelもリリースノートから最新バージョンを確認して依存関係に追加します(ここでは2020年7月時点の最新バージョンである2.2.0を導入します)。

dependencies {
    implementation "com.google.android.play:core:1.8.0"
    implementation "androidx.lifecycle:lifecycle-livedata:2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"
}

in-app updatesの各状態を定義する

in-app updatesには、ダウンロードやインストールに関するさまざまな状態があります。
状態をLiveDataで通知できるようにするため、各状態をあらかじめ定義します。

sealed class InAppUpdatesState {
    /** アップデートが無い */
    object UpdateNotAvailable : InAppUpdatesState()
    /** アップデートをリクエスト */
    data class RequestUpdate(val appUpdateInfo: AppUpdateInfo) : InAppUpdatesState()
    /** アップデートの再開をリクエスト */
    data class RequestResume(val appUpdateInfo: AppUpdateInfo) : InAppUpdatesState()
    /** ユーザーがアップデートを許可 */
    object Accept : InAppUpdatesState()
    /** ダウンロード中 */
    data class Downloading(
        val bytesDownloaded: Long,
        val totalBytesToDownload: Long
    ) : InAppUpdatesState() : InAppUpdatesState()
    /** ダウンロード完了 */
    object Downloaded : InAppUpdatesState()
    /** インストール中 */
    object Installing : InAppUpdatesState()
    /** ダウンロード保留中(通信状態が悪いなど) */
    object Pending : InAppUpdatesState()
    /** アップデートがキャンセルされた */
    object Cancel : InAppUpdatesState()
    /** アップデートに失敗した */
    object Failed : InAppUpdatesState()
}

in-app updatesのアップデート情報を取得した際に、アップデート情報はAppUpdateInfoクラスで返却されます。AppUpdateInfoはアプリ内アップデートを開始する際に必要です。
このように状態に対して値を持たせたいケースがいくつかあるため、enum classではなくsealed classで定義します。

ViewModelを実装する

ViewModelでは、in-app updatesからのコールバックを受け取り、状態をLiveDataに反映する処理を実装します。

AppUpdateManagerの初期化

in-app updatesに関する情報を取得するには、AppUpdateManagerクラスを使います。
AppUpdateManagerのインスタンスを取得するにはApplication Contextが必要です。
ViewModelの引数からApplicationを取得してAppUpdateManagerのインスタンスを取得するには次のようにします。

class InAppUpdatesViewModel(application: Application) : ViewModel() {
    private val appUpdateManager = AppUpdateManagerFactory.create(application)
}

AppUpdateManagerのインスタンスが取得できたら、これを使ってアップデート情報の取得などを行います。

アップデート情報を取得する

アプリのアップデートがあるかの判定や、現在アップデートの途中なのかなどのアップデート情報を取得するには、AppUpdateManager#getAppUpdateInfo()を使います。
AppUpdateManager#getAppUpdateInfo()は次のように実装します。

class InAppUpdatesViewModel(application: Application) : ViewModel(),
    LifecycleObserver,
    OnSuccessListener<AppUpdateInfo> {
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume() {
        appUpdateManager.appUpdateInfo.addOnSuccessListener(this /** OnSuccessListener */)
    }
    override fun onSuccess(appUpdateInfo: AppUpdateInfo) {
        // アップデート情報を取得してLiveDataなどに通知する
    }
}

アプリがフォアグラウンドにきたときにアップデートを通知できるように、アップデート情報の取得はonResumeで行うのがおすすめです。
ViewModel内でonResumeイベントを検知できるように、LifecyleObserverを実装して@OnLifecyleEventを任意のメソッドにつけます。
今回はonResumeメソッドを作成して@OnLifecycleEventをつけています。

onResumeメソッドの中でAppUpdateManager#getAppUpdateInfo()を呼び出し、アップデート情報を取得します。取得した結果はOnSuccessListenerで受け取ります。
取得に成功するとonSuccessメソッドが呼び出されるので、このメソッドの中で結果をLiveDataに通知するなど、必要な処理を行います。

インストール状況を取得する

インストールを開始したあと、ダウンロード中やインストール中などの状況を監視するにはInstallStateUpdatedListenerを実装します。

class InAppUpdatesViewModel(application: Application) : ViewModel(),
    LifecycleObserver,
    OnSuccessListener<AppUpdateInfo>,
    InstallStateUpdatedListener {
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreate() {
        appUpdateManager.registerListener(this /** InstallStateUpdatedListener */)
    }
    override fun onCleared() {
        super.onCleared()
        appUpdateManager.unregisterListener(this /** InstallStateUpdatedListener */)
    }
    override fun onStateUpdate(state: InstallState) {
    }
}

InstallStateUpdatedListenerはAppUpdateManager#registerListenerで登録します。ここではonCreate時に登録しています。
使い終わったらListenerの登録の解除を行います。登録の解除にはAppUpdateManager#unregisterListenerを使います。
ここではViewModelのonClearedで登録を解除することで、ViewModelが生きている間はイベントが通知されるようにしています。
コールバックはonStateUpdateメソッドに通知されるので、このメソッドを使ってインストール状況によって何らかの処理を行います。

FlexibleとImmediateを切り替える

1つのViewModel内でFlexibleとImmediateの両方に対応するには、いま現在どちらのアップデート方式なのかを保持しておく必要があります。
ここでは、ViewModel内にappUpdateTypeという変数を定義してアップデート方式を保持できるようにします。

private var appUpdateType: Int = -1
    get() {
        if (field == -1) throw UninitializedPropertyAccessException(
            "appUpdateType is not initialized. Please set flexible or immediate."
        )
        return field
    }

fun setupImmediateUpdate() {
    appUpdateType = AppUpdateType.IMMEDIATE
}

fun setupFlexibleUpdate() {
    appUpdateType = AppUpdateType.FLEXIBLE
}

アップデート方式は必ず設定してほしいので、カスタムプロパティを使ってappUpdateTypeが初期値のままであれば例外を投げるようにします。
アップデート方式の設定はメソッド経由でできるようにそれぞれメソッドを用意します。

in-app updatesの通知タイミングを制御する

in-app updatesでは、Google Playにアップデートが通知されてからの日数を確認できます。
Flexibleアップデートをユーザーに通知する前に数日待ってから通知したり、Immediateアップデートにより即時アップデートを行ってほしい重要なアップデートの場合は待たずにすぐに通知するなどの判断をするのに利用できます。
たとえばFlexibleアップデートの場合に、Google Playにアップデートが通知されてから2日待ってユーザーに通知するためには次のように実装します。

// Google Playにアップデート通知が届いてから何日後にアプリ内アップデートを通知するか
private const val DAYS_FOR_FLEXIBLE_UPDATE = 2

private fun flexibleUpdateAvailable(appUpdateInfo: AppUpdateInfo): Boolean {
    // デバッグアプリなら無条件でtrue
    if (BuildConfig.DEBUG) {
        return appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
    }
    val clientVersionStalenessDays = appUpdateInfo.clientVersionStalenessDays()
    return clientVersionStalenessDays != null &&
        clientVersionStalenessDays >= DAYS_FOR_FLEXIBLE_UPDATE &&
        appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)
}

はじめに、Flexibleアップデート方式でのアップデートが許可されているかを確認します。
アップデート許可はAppUpdateInfo#isUpdateTypeAllowed()で確認できます。引数にはAppUpdateType.FLEXIBLEまたはAppUpdateType.IMMEDIATEを渡します。ここではFlexibleアップデート方式でのアップデートが有効かを返却したいので、AppUpdateType.FLEXIBLEを渡します。

Google Playからアップデート通知が届いてからの日数はAppUpdateInfo#clientVersionStalenessDays()で取得できます。このメソッドはアップデートが届いていなければnullが返却されるため、先にnullかどうかを確認してから日数を判定します。今回は2日以上経過していればtrueを返すようにしています。

これらの条件はデバッグする際にも指定していると必ずfalseとなってしまうため、デバッグ中であれば必ずtrueを返すようにするなどの対応を入れておくとよいでしょう。

アップデートの優先度を確認して通知する

Google Play Developer APIを使ってアプリをアップロードする際、リリースするアプリのアプリ内アップデートの優先度を設定できます。
優先度は0から5までの値を設定することができます。最高の優先順位は5で、デフォルトの優先度は0となっています。
アプリ内アップデートの優先度を設定することで、重要なセキュリティアップデートの場合はImmediateアップデートを使ってアプリ内アップデートを行うなどの判断に利用できます。
たとえば、Immediateアップデートで優先度が4以上の場合にユーザーに通知するには次のようにします。

/** Immediateアップデート対象の優先度 */
private const val HIGH_PRIORITY_UPDATE = 4

private fun immediateUpdateAvailable(appUpdateInfo: AppUpdateInfo): Boolean {
    // デバッグアプリなら無条件でtrue
    if (BuildConfig.DEBUG) {
        return appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
    }
    return appUpdateInfo.updatePriority() >= HIGH_PRIORITY_UPDATE &&
        appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
}

優先度を取得するにはAppUpdateInfo#updatePriority()を使います。ここでは4以上の場合にtrueを返すようにしています。
アップデート許可の確認はFlexibleのときと同様で、今回はImmediateの確認をしたいので引数にAppUpdateType.IMMEDIATEを渡します。
これで、優先度が高い場合のみImmediateアップデートを有効にする判定ができるようになりました。

もしアプリのリリースにGradle Play Publisherを利用している場合は、2.8.0からupdatePriorityの設定ができるようになっています。アプリ内アップデートを提供する際には2.8.0以上にアップデートしておくとよいでしょう。

in-app updatesのアップデート状態を通知する

in-app updatesのアップデート状態を通知する処理を実装していきます。
アップデート状態は最初に作成したsealed classのInAppUpdatesStateを使います。
LiveDataでInAppUpdatesStateを監視するため、次のように実装します。

private val _state = MutableLiveData<InAppUpdatesState>(InAppUpdatesState.UpdateNotAvailable)
val state: LiveData<InAppUpdatesState> = _state

ViewModel内でのみ状態の変更ができるようにMutableLiveDataはprivateで実装し、状態の監視が外からできるようにLiveDataで外に公開します。

次に、取得したアップデート状態をLiveDataに通知する処理を見ていきましょう。
アップデート状態の取得は前述したOnSuccessListenerのonSuccessを使って受け取れるので、onSuccessの中身を実装します。

override fun onSuccess(appUpdateInfo: AppUpdateInfo) {
    when (appUpdateInfo.updateAvailability()) {
        UpdateAvailability.UPDATE_AVAILABLE -> {}
        UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> {}
        UpdateAvailability.UPDATE_NOT_AVAILABLE -> {}
        UpdateAvailability.UNKNOWN -> {}
    }
}

AppUpdateInfo#updateAvailability()で現在のアップデート状態を取得できます。
intで返却されるため、UpdateAvailabilityに定義されている定数によって状態を判別します。
各定数は次のように定義されています。

定数 説明
UpdateAvailability.UPDATE_AVAILABLE アップデート可能な状態
UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS ダウンロードやインストールが開始されているアップデート中の状態
UpdateAvailability.UPDATE_NOT_AVAILABLE アップデートが無い状態
UpdateAvailability.UNKNOWN 上記以外の不明な状態

各状態ごとの実装を順番に見ていきましょう。まずはアップデート可能な状態のときです。

override fun onSuccess(appUpdateInfo: AppUpdateInfo) {
    when (appUpdateInfo.updateAvailability()) {
        UpdateAvailability.UPDATE_AVAILABLE -> {
            val requestUpdate = if (appUpdateType == AppUpdateType.FLEXIBLE) {
                flexibleUpdateAvailable(appUpdateInfo)
            } else {
                immediateUpdateAvailable(appUpdateInfo)
            }
            if (requestUpdate) {
                _state.postValue(InAppUpdatesState.RequestUpdate(appUpdateInfo))
            } else {
                _state.postValue(InAppUpdatesState.UpdateNotAvailable)
            }
        }
        // …省略…
    }
}

ViewModelに事前に設定されているアップデート方式ごとに通知可能な状態かを判別し、
通知可能な状態であればRequestUpdateをpostしてアップデート可能であることを通知します。
それ以外の場合はアップデートが無い状態としてUpdateNotAvailableをpostします。

今回のサンプルではViewModelの初期化時にアップデート方式を設定して、設定されたアップデート方式によってアップデートを通知するかを判定していますが、もしGoogle Playにアップデートが届いてからの経過日数やアップデートの優先度を元にアップデート方式を変更する場合はそういった処理をアップデート可能な状態のときに行うとよいでしょう。

次に、ダウンロードやインストールが開始されているアップデート中の状態のときです。

override fun onSuccess(appUpdateInfo: AppUpdateInfo) {
    when (appUpdateInfo.updateAvailability()) {
        // …省略…
        UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> {
            if (appUpdateType == AppUpdateType.FLEXIBLE) {
                // Flexibleですでにアップデート中の場合はonStateUpdateで通知するので何もしない
                return
            } else {
                _state.postValue(InAppUpdatesState.RequestResume(appUpdateInfo))
            }
        }
    }
}

アップデート中の場合、ダウンロードやインストールの状況はonStateUpdateのInstallStateクラスから詳しい状況を取得できます。そのため、バックグラウンドでダウンロード処理が走るFlexibleアップデート方式の場合はonStateUpdateを使って状況を通知したほうがよいです。
Immediateアップデート方式の場合は継続してアプリを使うことが想定されていないため、ダウンロードやインストール中にアプリに戻ってきてしまった場合にImmediateアップデートの画面に戻せるようにアップデートを再開するRequestResumeをpostしています。

最後にアップデートが無い状態の場合です。

override fun onSuccess(appUpdateInfo: AppUpdateInfo) {
    when (appUpdateInfo.updateAvailability()) {
        // …省略…
        UpdateAvailability.UPDATE_NOT_AVAILABLE,
        UpdateAvailability.UNKNOWN ->
            _state.postValue(InAppUpdatesState.UpdateNotAvailable)
    }
}

アップデートが無い状態やそれ以外の不明な状態の場合はどちらもアップデートが無い状態として UpdateNotAvailable をpostしています。このときは何も通知する必要がないのでUI側では特に何も表示しなくてもよいでしょう。

インストール状況を通知する

アプリ内アップデートのインストール状況はInstallStateUpdatedListenerのonStartUpdateを使って取得できます。
ダウンロード中やインストール中などの状況を判別するには次のように実装します。

override fun onStateUpdate(state: InstallState) {
    when (state.installStatus()) {
        InstallStatus.DOWNLOADED -> InAppUpdatesState.Downloaded
        // bytesDownloadedとtotalBytesToDownloadはDOWNLOADING以外は0が返却される
        InstallStatus.DOWNLOADING -> InAppUpdatesState.Downloading(
            state.bytesDownloaded(),
            state.totalBytesToDownload()
        )
        InstallStatus.PENDING -> InAppUpdatesState.Pending
        InstallStatus.INSTALLING -> InAppUpdatesState.Installing
        else -> null
    }?.let {
        _state.postValue(it)
    }
}

現在ダウンロード中なのか、インストール中なのかはInstallState#installStatus()からintで取得できます。
各状態はInstallStatusの定数によって判別できるので、各状態に応じたInAppUpdatesStateをpostしています。
現在ダウンロード済のファイルサイズはInstallState#bytesDownloaded()、ダウンロードするファイルの合計サイズはInstallState#totalBytesToDownload()で取得できます。ダウンロード中の場合はこれらの値も一緒にpostしています。
これら2つの値を使うことで、現在何%ダウンロード済なのかを表示する際に利用できます。
ダウンロード中以外の場合はこれら2つの値は0が返却されるため、ダウンロード中以外の場合はpostしていません。

in-app updatesを開始する

in-app updatesを開始するには、次のように実装します。

fun startUpdateFlowForResult(appUpdateInfo: AppUpdateInfo, activity: Activity) {
    appUpdateManager.startUpdateFlowForResult(
        appUpdateInfo,
        appUpdateType,
        activity,
        REQUEST_CODE_IN_APP_UPDATES
    )
}

第一引数のappUpdateInfoにはOnSuccessListenerで取得したAppUpdateInfoクラスのインスタンスを渡します。
第二引数のappUpdateTypeにはFlexibleまたはImmediateのどちらのアプリ内アップデートを行うのかをintで指定します。
第三引数、第四引数にはアプリ内アップデート画面に遷移するために必要なActivityと、startActivityForResultに必要なREQUEST_CODEを渡します。
これを実行すると、Flexibleの場合はアップデートダイアログ、Immediateの場合は全画面でアップデート画面が表示されます。

アプリ内アップデートのActivityから帰ってきたときの処理は次のように実装します。

fun onActivityResult(requestCode: Int, resultCode: Int) {
    if (requestCode == REQUEST_CODE_IN_APP_UPDATES) {
        when (resultCode) {
            Activity.RESULT_OK -> _state.postValue(InAppUpdatesState.Accept)
            Activity.RESULT_CANCELED -> _state.postValue(InAppUpdatesState.Cancel)
            ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> _state.postValue(InAppUpdatesState.Failed)
        }
    }
}

requestCodeはstartUpdateFlowForResultに渡したrequestCodeで判別します。
RESULT_OKは、Flexibleアップデートの場合はユーザーが更新ボタンを押した後に呼び出され、Immediateの場合はユーザーが更新ボタンを押した後に端末の戻るボタンなどでアプリに戻ってきてしまった場合に返ります。
更新開始後にアプリに戻ってきた時点でアップデート中の状態がOnSuccessListenerから取得できるので、RESULT_OKのときに何かをするよりonSuccessで何らかの処理を行うほうがよいでしょう。
RESULT_CANCELEDは、Flexibleの場合は「ダウンロードしない」を押したとき、Immediateの場合は右上のバツボタンを押したときに返ります。キャンセルされた場合は数日後に再度表示するなど、アプリの要件によってアプリ内アップデートの再表示を制御したいときなどに利用するとよいでしょう。
RESULT_IN_APP_UPDATE_FAILEDはアプリ内アップデートがエラーになった場合に返ります。この場合はエラーになったことをユーザーに通知してあげるとよいでしょう。

各状態をLiveDataにpostしているので、これをActivityやFragmentのonActivityResultで呼び出すだけでLiveDataで状態を監視できます。

アップデートを反映する

Flexibleアップデートの場合は、アップデートデータのダウンロードが完了したあとにアップデートを反映してあげる必要があります。
アップデートの反映は次のようにします。

fun completeUpdate() {
    appUpdateManager.completeUpdate()
}

AppUpdateManager#completeUpdate()を呼び出すとインストール画面に遷移してインストールが開始されます。
インストールが終わるとアプリが自動で再起動されるので、これを呼び出すだけでアップデートの反映が行われます。

UIを実装する

ViewModelの実装が終わったので、最後にUIを実装していきます。
UI側ではin-app updatesの状態をLiveDataで監視し、状態に応じてユーザーに通知します。

ViewModelの初期化

まずはViewModelの初期化です。

class InAppUpdatesActivity : AppCompatActivity() {
    private lateinit var inAppUpdatesViewModel: InAppUpdatesViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        inAppUpdatesViewModel = ViewModelProvider(this, viewModelFactory)
            .get(InAppUpdatesViewModel::class.java)
            // LifecycleObserverに登録する
            .apply(lifecycle::addObserver)
            // アップデート方式を選択する
            .apply(InAppUpdatesViewModel::setupFlexibleUpdate)
    }
}

LifecycleObserverへの登録とアップデート方式の選択は忘れないように実装しましょう。

アップデート状態に応じてUIに反映する

ViewModelが実装できたらアップデート状態をLiveDataで監視して、状態に応じてUIに反映します。

inAppUpdatesViewModel.state.observeNonNull(this) { state ->
    // 各状態に必要に応じてUIに反映する
    when(state) {
        is InAppUpdatesState.RequestUpdate ->
            inAppUpdatesViewModel.startUpdateFlowForResult(state.appUpdateInfo, activity)
        is InAppUpdatesState.RequestResume ->
            inAppUpdatesViewModel.startUpdateFlowForResult(state.appUpdateInfo, activity)
        is InAppUpdatesState.Downloading -> TODO()
        InAppUpdatesState.UpdateNotAvailable -> TODO()
        InAppUpdatesState.Accept -> TODO()
        InAppUpdatesState.Downloaded -> TODO()
        InAppUpdatesState.Installing -> TODO()
        InAppUpdatesState.Pending -> TODO()
        InAppUpdatesState.Cancel -> TODO()
        InAppUpdatesState.Failed -> TODO()
    }    
}

今回はアップデートのリクエストなどがきたらすぐにアップデート画面を表示するような実装をしていますが、アップデートがある場合は画面にアップデートがあることをユーザーに通知してボタンを押したらアップデート画面を表示したいケースも多いでしょう。そのようなケースではLiveDataをmapして特定の条件になったらUI側に反映するなど、柔軟に実装してあげるとよいでしょう。

アップデート結果の受け取り

アップデートのキャンセルなどはonActivityResultで受け取るので、忘れないように実装しておきましょう。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    inAppUpdatesViewModel.onActivityResult(requestCode, resultCode)
}

これで実装はおわりです。

in-app updatesの検証方法

in-app updatesはGoogle Playへの配信が必要な都合上、ベータ版やアルファ版または内部テスト版トラックに配信する必要があります。
ベータ版やアルファ版または内部テスト版トラックへの配信は毎回バージョンコードを上げなければならず、デバッグをする際に利用するにはとても非効率です。
in-app updatesではInternal app sharingを使ってテストをすることができるので、デバッグ時にはInternal app sharingを使ってテストをするのがよいです。

Internal app sharingを使って検証するには次の手順でアプリのアップロード、インストールを行います。

  1. アプリ内アップデート機能を実装したアプリをInternal app sharingにアップロードする
  2. 上記のアプリよりも高いバージョンコードのアプリをInternal app sharingにアップロードする
  3. 1のアプリをInternal app sharing経由でインストールする
  4. 1のアプリにて、in-app updates機能が実装された画面が表示できるところまでいけるように、ログイン等が必要な場合は先に済ませておく
  5. 2のアプリのInternal app sharingのリンクだけ開く(開くだけでインストールしない
    • これによりアップデートがあるという状態になる
  6. 1のアプリでin-app updates機能が表示されるように操作する
  7. これでGoogle Playのin-app updatesの画面が表示される

重要なのは、5のアップデートがある状態を作る部分です。
バージョンコードが新しいアプリのInternal app sharingの配信URLを開くことで、Google Playに対して更新があることを伝えています。これがうまくいっていればアプリの更新があることになるので、in-app updatesの実装がされている画面でin-app updatesの検証ができます。

おわりに

Android版「マネーフォワード ME」におけるin-app updatesの実装と、AACを利用したin-app updatesのサンプル実装、そしてin-app updatesの検証方法について解説してきました。
in-app updatesを利用することで、Playストアに遷移する手間がなくアプリ内でアプリの更新を行えるため、うまく活用することで自動更新をONにしていないユーザーなどにもアップデートをしてもらえる可能性が高まります。
アプリの新機能や不具合修正などをユーザーにいち早く提供するためにも、こういった仕組みを使って最高のアップデート体験を実現していきましょう。

マネーフォワードでは、エンジニアを募集しています。
ご応募お待ちしています。

【サイトのご案内】
マネーフォワード採用サイト
Wantedly
京都開発拠点

【プロダクトのご紹介】
お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android

ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』

おつり貯金アプリ 『しらたま』

お金の悩みを無料で相談 『マネーフォワード お金の相談』

だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』

金融商品の比較・申し込みサイト 『Money Forward Mall』

くらしの経済メディア 『MONEY PLUS』

Pocket