Skip to content

Commit

Permalink
Initial implementation of In-App Updates
Browse files Browse the repository at this point in the history
  • Loading branch information
keyboardsurfer committed Jul 9, 2019
1 parent 8572b9c commit 50c39b8
Show file tree
Hide file tree
Showing 6 changed files with 512 additions and 0 deletions.
109 changes: 109 additions & 0 deletions app/src/main/java/io/plaidapp/ui/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import android.app.ActivityOptions
import android.content.Context
import android.content.Intent
import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Build
import android.os.Bundle
import android.text.Annotation
import android.text.Spannable
Expand All @@ -32,6 +33,7 @@ import android.text.SpannedString
import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
import android.transition.TransitionManager
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
Expand All @@ -57,6 +59,12 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import com.bumptech.glide.util.ViewPreloadSizeProvider
import com.google.android.material.snackbar.Snackbar
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.InstallState
import com.google.android.play.core.install.InstallStateUpdatedListener
import com.google.android.play.core.install.model.AppUpdateType
import io.plaidapp.R
import io.plaidapp.core.dagger.qualifier.IsPocketInstalled
import io.plaidapp.core.data.prefs.SourcesRepository
Expand All @@ -81,6 +89,11 @@ import io.plaidapp.core.util.intentTo
import io.plaidapp.dagger.inject
import io.plaidapp.ui.recyclerview.FilterTouchHelperCallback
import io.plaidapp.ui.recyclerview.GridItemDividerDecoration
import io.plaidapp.util.checkForUpdate
import io.plaidapp.util.onActivityResult
import io.plaidapp.util.onInstalled
import io.plaidapp.util.updateFlexibly
import io.plaidapp.util.updateImmediately
import javax.inject.Inject

/**
Expand Down Expand Up @@ -184,6 +197,10 @@ class HomeActivity : AppCompatActivity() {
}
}

private val appUpdateManager by lazy {
AppUpdateManagerFactory.create(this)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
Expand Down Expand Up @@ -236,6 +253,11 @@ class HomeActivity : AppCompatActivity() {
it.attachToRecyclerView(filtersList)
}
checkEmptyState()

appUpdateManager.checkForUpdate(
immediateUpdate = ::performImmediateUpdate,
flexibleUpdate = ::performFlexibleUpdate
)
}

private fun initViewModelObservers() {
Expand Down Expand Up @@ -515,6 +537,91 @@ class HomeActivity : AppCompatActivity() {
}
}
}
RC_APP_UPDATE_CHECK -> {
// TODO define handling and handle these cases properly.
appUpdateManager.onActivityResult(
resultCode, { Log.i(TAG, "User accepted update") },
{ Log.e(TAG, "User cancelled update.") },
{ Log.e(TAG, "In App Update failed with result code $resultCode.") }
)
}
}
}

/**
* Perform an In App Update depending on type.
*
* @param appUpdateInfo The [AppUpdateInfo] received.
* @param type either of the [AppUpdateType] values.
* @param updateReady Called once the update is installed.
*/
private fun performInAppUpdate(
appUpdateInfo: AppUpdateInfo,
@AppUpdateType type: Int,
updateReady: () -> Unit
) {
val listener = object : InstallStateUpdatedListener {
override fun onStateUpdate(state: InstallState) {
state.onInstalled {
appUpdateManager.unregisterListener(this)
updateReady()
}
}
}

with(appUpdateManager) {
registerListener(listener)

val homeActivity = this@HomeActivity
when (type) {
AppUpdateType.IMMEDIATE -> updateImmediately(homeActivity, RC_APP_UPDATE_CHECK)
AppUpdateType.FLEXIBLE -> updateFlexibly(homeActivity, RC_APP_UPDATE_CHECK)
}
}
}

private fun performImmediateUpdate(appUpdateInfo: AppUpdateInfo) {
/*
This is a basic check, which will be replaced with a more sophisticated one in the
future.
Instead of relying simply on the version code difference to check whether an update
is required immediately, other apps might want to defer to a server that provides the
signal required to decide on which update path should be followed.
*/
if (appUpdateInfo.availableVersionCode() - getVersionCode() > 100) {
performInAppUpdate(appUpdateInfo, AppUpdateType.IMMEDIATE) {
appUpdateManager.completeUpdate()
}
}
}

private fun performFlexibleUpdate(appUpdateInfo: AppUpdateInfo) {
performInAppUpdate(appUpdateInfo, AppUpdateType.FLEXIBLE) {
flexibleUpdateReady()
}
}

private fun flexibleUpdateReady() {
// TODO("Add flexible in-app update tile as shown in #703")
Snackbar.make(
findViewById(R.id.home_frame), getString(R.string.update_downloaded),
Snackbar.LENGTH_INDEFINITE
)
.setAction(getString(R.string.snackbar_restart)) { appUpdateManager.completeUpdate() }
.show()
}

/**
* Get the versionCode for this installed application.
*/
private fun getVersionCode(): Long {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
@Suppress("DEPRECATION")
packageInfo.versionCode.toLong()
}
}

Expand Down Expand Up @@ -644,6 +751,8 @@ class HomeActivity : AppCompatActivity() {

private const val RC_SEARCH = 0
private const val RC_NEW_DESIGNER_NEWS_LOGIN = 5
private const val RC_APP_UPDATE_CHECK = 6
private const val TAG = "HomeActivity"
}
}

Expand Down
207 changes: 207 additions & 0 deletions app/src/main/java/io/plaidapp/util/AppUpdateManagerExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
* Copyright 2019 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.plaidapp.util

import android.app.Activity
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.install.InstallStateUpdatedListener
import com.google.android.play.core.install.model.ActivityResult
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability
import com.google.android.play.core.tasks.OnSuccessListener

// Update checks

/**
* Checks for an update and performs an action based on any of the update availability states.
*/
inline fun AppUpdateManager.checkForUpdate(
crossinline noUpdateAvailable: (info: AppUpdateInfo) -> Unit = {},
crossinline updateInProgress: (info: AppUpdateInfo) -> Unit = {},
crossinline flexibleUpdate: (info: AppUpdateInfo) -> Unit = {},
crossinline immediateUpdate: (info: AppUpdateInfo) -> Unit = {}
) {
val listener = OnSuccessListener<AppUpdateInfo> { info ->
when (info.updateAvailability()) {
UpdateAvailability.UPDATE_AVAILABLE -> {
if (info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
flexibleUpdate(info)
} else if (info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
immediateUpdate(info)
}
}
UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> updateInProgress(info)
else -> noUpdateAvailable(info)
}
}
appUpdateInfo.addOnSuccessListener(listener)
}

inline fun AppUpdateManager.doOnImmediateUpdate(crossinline action: (info: AppUpdateInfo) -> Unit) =
checkForUpdate(immediateUpdate = action)

inline fun AppUpdateManager.doOnFlexibleUpdate(crossinline action: (info: AppUpdateInfo) -> Unit) =
checkForUpdate(flexibleUpdate = action)

inline fun AppUpdateManager.doOnNoUpdate(crossinline action: (info: AppUpdateInfo) -> Unit) =
checkForUpdate(noUpdateAvailable = action)

inline fun AppUpdateManager.doOnUpdateInProgress(
crossinline action: (info: AppUpdateInfo) -> Unit
) = checkForUpdate(updateInProgress = action)

inline fun AppUpdateManager.doOnAppUpdateInfoRetrieved(
crossinline action: (info: AppUpdateInfo) -> Unit
) = checkForUpdate(action, action, action)

// Update the app

/**
* Update the app for an update of type [AppUpdateType.FLEXIBLE].
*/
fun AppUpdateManager.updateFlexibly(activity: Activity, resultCode: Int) {
doOnFlexibleUpdate {
startUpdateFlowForResult(
it,
AppUpdateType.IMMEDIATE,
activity,
resultCode
)
}
}

/**
* Update the app for an update of type [AppUpdateType.IMMEDIATE].
*/
fun AppUpdateManager.updateImmediately(activity: Activity, resultCode: Int) {
doOnImmediateUpdate {
startUpdateFlowForResult(
it,
AppUpdateType.IMMEDIATE,
activity,
resultCode
)
}
}

/**
* Update the app for a given update type.
*
* @param activity The activity that performs the update.
* @param resultCode The result code to use within your activity's onActivityResult.
* @param type The type of update to perform.
*/
fun AppUpdateManager.update(
activity: Activity,
resultCode: Int,
@AppUpdateType type: Int
) {
doOnAppUpdateInfoRetrieved {
if (it.isUpdateTypeAllowed(type)) {
if (it.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
updateFlexibly(activity, resultCode)
} else if (it.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
updateImmediately(activity, resultCode)
}
}
// TODO handle update type not allowed flow.
}
}

// Install state handling

/**
* Perform an action on any provided install state.
*/
inline fun AppUpdateManager.doOnInstallState(
crossinline onUnknown: (errorCode: Int) -> Unit = {},
crossinline onCanceled: (errorCode: Int) -> Unit = {},
crossinline onFailed: (errorCode: Int) -> Unit = {},
crossinline onRequiresUiIntent: () -> Unit = {},
crossinline onPending: () -> Unit = {},
crossinline onDownloading: () -> Unit = {},
crossinline onDownloaded: () -> Unit = {},
crossinline onInstalling: () -> Unit = {},
crossinline onInstalled: () -> Unit = {}
): InstallStateUpdatedListener {
return InstallStateUpdatedListener {
it.onStatus(
onUnknown = onUnknown,
onCanceled = onCanceled,
onFailed = onFailed,
onRequiresUiIntent = onRequiresUiIntent,
onPending = onPending,
onDownloading = onDownloading,
onDownloaded = onDownloaded,
onInstalling = onInstalling,
onInstalled = onInstalled
)
}
}

inline fun AppUpdateManager.onInstallStateUnknown(crossinline onUnknown: (errorCode: Int) -> Unit) =
doOnInstallState(onUnknown = onUnknown)

inline fun AppUpdateManager.onInstallStateCanceled(
crossinline onCanceled: (errorCode: Int) -> Unit
) =
doOnInstallState(onCanceled = onCanceled)

inline fun AppUpdateManager.onInstallStateFailed(crossinline onFailed: (errorCode: Int) -> Unit) =
doOnInstallState(onFailed = onFailed)

inline fun AppUpdateManager.onInstallStateRequiresUiIntent(
crossinline onRequiresUiIntent: () -> Unit
) =
doOnInstallState(onRequiresUiIntent = onRequiresUiIntent)

inline fun AppUpdateManager.onInstallStatePending(crossinline onPending: () -> Unit) =
doOnInstallState(onPending = onPending)

inline fun AppUpdateManager.onInstallStateDownloading(
crossinline onDownloading: () -> Unit
) =
doOnInstallState(onDownloading = onDownloading)

inline fun AppUpdateManager.onInstallStateDownloaded(
crossinline onDownloaded: () -> Unit
) =
doOnInstallState(onDownloaded = onDownloaded)

inline fun AppUpdateManager.onInstallStateRequiresInstalling(
crossinline onInstalling: () -> Unit
) =
doOnInstallState(onInstalling = onInstalling)

inline fun AppUpdateManager.onInstallStateInstalled(
crossinline onInstalled: () -> Unit
) =
doOnInstallState(onInstalled = onInstalled)

inline fun AppUpdateManager.onActivityResult(
resultCode: Int,
accepted: () -> Unit,
canceled: () -> Unit,
failed: () -> Unit
) {
when (resultCode) {
Activity.RESULT_OK -> accepted()
Activity.RESULT_CANCELED -> canceled()
ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> failed()
}
}
Loading

0 comments on commit 50c39b8

Please sign in to comment.