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 8, 2019
1 parent 8572b9c commit d683a28
Show file tree
Hide file tree
Showing 6 changed files with 511 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
206 changes: 206 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,206 @@
/*
* 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 -> {
when (info.updateAvailability()) {

This comment has been minimized.

Copy link
@Archive-Patcher-Sync-Robot

Archive-Patcher-Sync-Robot Jul 8, 2019

is info.updateAvailability() annotated with AppUpdateType?

This comment has been minimized.

Copy link
@keyboardsurfer

keyboardsurfer Jul 9, 2019

Author Collaborator

It's annotated with UpdateAvailability.

This comment has been minimized.

Copy link
@Archive-Patcher-Sync-Robot

Archive-Patcher-Sync-Robot Jul 9, 2019

Yes. In this line (and two lines below), are we "swtiching" on the values of info.updateAvailability as if it is annotated with AppUpdateType?

This comment has been minimized.

Copy link
@keyboardsurfer

keyboardsurfer Jul 9, 2019

Author Collaborator

Fixed. Discussing w/o a PR is tedious. I drafted one here: #746

AppUpdateType.FLEXIBLE -> flexibleUpdate(info)
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 any available 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

This comment has been minimized.

Copy link
@pspencil

pspencil Jul 8, 2019

what is the purpose of type here if we just update for "any available type"?

This comment has been minimized.

Copy link
@keyboardsurfer

keyboardsurfer Jul 9, 2019

Author Collaborator

Some documentation may be out of date.

This comment has been minimized.

Copy link
@keyboardsurfer

This comment has been minimized.

Copy link
@Archive-Patcher-Sync-Robot

Archive-Patcher-Sync-Robot Jul 9, 2019

the code below checks if the type is allowed. Then proceeds to check if either type is allowed. What is the desired behaviour of this method?

This comment has been minimized.

Copy link
@Archive-Patcher-Sync-Robot

Archive-Patcher-Sync-Robot Jul 9, 2019

Ah. I see. I suggest instead of checking if FLEXIBLE or IMMEDIATE are allowed, we do

if (it == AppUpdateType.FLEXIBLE) {
updateFlexibly(...);
} else if (it == AppUpdateType.IMMEDIATE) {
....
}
) {
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 d683a28

Please sign in to comment.