Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce PlayCore and In-App Updates #746

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ dependencies {
implementation project(':core')
implementation "androidx.appcompat:appcompat:${versions.appcompat}"
implementation "com.crashlytics.sdk.android:crashlytics:${versions.crashlytics}"
implementation "com.google.firebase:firebase-core:${versions.firebase}"
implementation "com.github.bumptech.glide:glide:${versions.glide}"
implementation "com.github.bumptech.glide:recyclerview-integration:${versions.glide}"
implementation "com.google.android.play:core:${versions.playCore}"
implementation "com.google.firebase:firebase-core:${versions.firebase}"

kapt "com.google.dagger:dagger-compiler:${versions.dagger}"
}
Expand Down
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
209 changes: 209 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,209 @@
/*
* 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 ->
with(info) {
when (updateAvailability()) {
UpdateAvailability.UPDATE_AVAILABLE -> {
if (isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
flexibleUpdate(this)
} else if (isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
immediateUpdate(this)
}
}
UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> updateInProgress(this)
else -> noUpdateAvailable(this)
}
}
}
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