diff --git a/app/src/main/java/io/plaidapp/ui/HomeActivity.kt b/app/src/main/java/io/plaidapp/ui/HomeActivity.kt index b4da3dfa5..08fbb7b15 100644 --- a/app/src/main/java/io/plaidapp/ui/HomeActivity.kt +++ b/app/src/main/java/io/plaidapp/ui/HomeActivity.kt @@ -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 @@ -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 @@ -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 @@ -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 /** @@ -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) @@ -236,6 +253,11 @@ class HomeActivity : AppCompatActivity() { it.attachToRecyclerView(filtersList) } checkEmptyState() + + appUpdateManager.checkForUpdate( + immediateUpdate = ::performImmediateUpdate, + flexibleUpdate = ::performFlexibleUpdate + ) } private fun initViewModelObservers() { @@ -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() } } @@ -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" } } diff --git a/app/src/main/java/io/plaidapp/util/AppUpdateManagerExtensions.kt b/app/src/main/java/io/plaidapp/util/AppUpdateManagerExtensions.kt new file mode 100644 index 000000000..6d85f07af --- /dev/null +++ b/app/src/main/java/io/plaidapp/util/AppUpdateManagerExtensions.kt @@ -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 { info -> + when (info.updateAvailability()) { + UpdateAvailability.UPDATE_AVAILABLE -> { + when (info.updateAvailability()) { + 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 +) { + 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() + } +} diff --git a/app/src/main/java/io/plaidapp/util/InstallStateExtensions.kt b/app/src/main/java/io/plaidapp/util/InstallStateExtensions.kt new file mode 100644 index 000000000..d3ea3fcb6 --- /dev/null +++ b/app/src/main/java/io/plaidapp/util/InstallStateExtensions.kt @@ -0,0 +1,83 @@ +/* + * 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 com.google.android.play.core.install.InstallState +import com.google.android.play.core.install.model.InstallStatus + +/** + * Performs an action on a given [InstallStatus]. + */ +inline fun InstallState.onStatus( + 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 = {} +) { + when (installStatus()) { + InstallStatus.UNKNOWN -> onUnknown(installErrorCode()) + InstallStatus.CANCELED -> onCanceled(installErrorCode()) + InstallStatus.FAILED -> onFailed(installErrorCode()) + InstallStatus.REQUIRES_UI_INTENT -> onRequiresUiIntent() + InstallStatus.PENDING -> onPending() + InstallStatus.DOWNLOADING -> onDownloading() + InstallStatus.DOWNLOADED -> onDownloaded() + InstallStatus.INSTALLING -> onInstalling() + InstallStatus.INSTALLED -> onInstalled() + } +} + +inline fun InstallState.onUnknownError(crossinline action: (errorCode: Int) -> Unit) { + onStatus(onUnknown = action) +} + +inline fun InstallState.onCanceled(crossinline action: (errorCode: Int) -> Unit) { + onStatus(onCanceled = action) +} + +inline fun InstallState.onFailed(crossinline action: (errorCode: Int) -> Unit) { + onStatus(onFailed = action) +} + +inline fun InstallState.onRequiresUiIntent(crossinline action: () -> Unit) { + onStatus(onRequiresUiIntent = action) +} + +inline fun InstallState.onPending(crossinline action: () -> Unit) { + onStatus(onPending = action) +} + +inline fun InstallState.onDownloading(crossinline action: () -> Unit) { + onStatus(onDownloading = action) +} + +inline fun InstallState.onDownloaded(crossinline action: () -> Unit) { + onStatus(onDownloaded = action) +} + +inline fun InstallState.onInstalling(crossinline action: () -> Unit) { + onStatus(onInstalling = action) +} + +inline fun InstallState.onInstalled(crossinline action: () -> Unit) { + onStatus(onInstalled = action) +} diff --git a/app/src/main/java/io/plaidapp/util/SplitInstallStateUpdatedListenerExtensions.kt b/app/src/main/java/io/plaidapp/util/SplitInstallStateUpdatedListenerExtensions.kt new file mode 100644 index 000000000..10933fbf7 --- /dev/null +++ b/app/src/main/java/io/plaidapp/util/SplitInstallStateUpdatedListenerExtensions.kt @@ -0,0 +1,110 @@ +/* + * 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 com.google.android.play.core.splitinstall.SplitInstallSessionState +import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus + +/** + * Performs an action on a [SplitInstallSessionStatus] update. + */ +inline fun SplitInstallSessionState.onStatus( + crossinline onUnknownError: (errorCode: Int) -> Unit = {}, + crossinline onCanceling: () -> Unit = {}, + crossinline onCanceled: () -> Unit = {}, + crossinline onFailed: () -> Unit = {}, + crossinline onRequiresConfirmation: () -> Unit = {}, + crossinline onPending: () -> Unit = {}, + crossinline onDownloading: (bytesDownloaded: Long, totalBytesToDownload: Long) -> Unit = + { _, + _ -> + }, + crossinline onDownloaded: () -> Unit = {}, + crossinline onInstalling: () -> Unit = {}, + crossinline onInstalled: () -> Unit = {} +) { + + when (status()) { + SplitInstallSessionStatus.UNKNOWN -> onUnknownError(errorCode()) + SplitInstallSessionStatus.CANCELING -> onCanceling() + SplitInstallSessionStatus.CANCELED -> onCanceled() + SplitInstallSessionStatus.FAILED -> onFailed() + SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> onRequiresConfirmation() + SplitInstallSessionStatus.PENDING -> onPending() + SplitInstallSessionStatus.DOWNLOADING -> onDownloading( + bytesDownloaded(), + totalBytesToDownload() + ) + SplitInstallSessionStatus.DOWNLOADED -> onDownloaded() + SplitInstallSessionStatus.INSTALLING -> onInstalling() + SplitInstallSessionStatus.INSTALLED -> onInstalled() + } +} + +inline fun SplitInstallSessionState.onUnknownError(crossinline action: (errorCode: Int) -> Unit) = + onStatus(onUnknownError = action) + +inline fun SplitInstallSessionState.onCanceling(crossinline action: () -> Unit) = + onStatus(onCanceling = action) + +inline fun SplitInstallSessionState.onCanceled(crossinline action: () -> Unit) = + onStatus(onCanceled = action) + +inline fun SplitInstallSessionState.onFailed(crossinline action: () -> Unit) = + onStatus(onFailed = action) + +inline fun SplitInstallSessionState.onRequiresConfirmation(crossinline action: () -> Unit) = + onStatus(onRequiresConfirmation = action) + +inline fun SplitInstallSessionState.onPending(crossinline action: () -> Unit) = + onStatus(onPending = action) + +inline fun SplitInstallSessionState.onDownloading( + crossinline action: ( + bytesDownloaded: Long, + totalBytesToDownload: Long + ) -> Unit +) = + onStatus(onDownloading = action) + +inline fun SplitInstallSessionState.onDownloaded(crossinline action: () -> Unit) = + onStatus(onDownloaded = action) + +inline fun SplitInstallSessionState.onInstalling(crossinline action: () -> Unit) = + onStatus(onInstalling = action) + +inline fun SplitInstallSessionState.onInstalled(crossinline action: () -> Unit) = + onStatus(onInstalled = action) + +inline fun SplitInstallSessionState.onHappyPath( + crossinline onPending: () -> Unit = {}, + crossinline onDownloading: (bytesDownloaded: Long, totalBytesToDownload: Long) -> Unit = + { _, + _ -> + }, + crossinline onDownloaded: () -> Unit = {}, + crossinline onInstalling: () -> Unit = {}, + crossinline onInstalled: () -> Unit = {} +) { + onStatus( + onPending = onPending, + onDownloading = onDownloading, + onDownloaded = onDownloaded, + onInstalling = onInstalling, + onInstalled = onInstalled + ) +} diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index 64401dc83..3220afd60 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -25,6 +25,7 @@ tools:context=".ui.HomeActivity"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6e3810d70..b9cc087df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,5 +38,7 @@ Search Dribbble & Designer News + An update has just been downloaded. + RESTART