diff --git a/demo-app/build.gradle.kts b/demo-app/build.gradle.kts index 6d836f5393..cc99eddd98 100644 --- a/demo-app/build.gradle.kts +++ b/demo-app/build.gradle.kts @@ -267,6 +267,9 @@ dependencies { // Only used for launching a QR code scanner in demo app implementation(libs.play.code.scanner) + // Play in-app updates + implementation(libs.play.app.update.ktx) + // memory detection debugImplementation(libs.leakCanary) diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/MainActivity.kt b/demo-app/src/main/kotlin/io/getstream/video/android/MainActivity.kt index 6c769ab1aa..0f04996d32 100644 --- a/demo-app/src/main/kotlin/io/getstream/video/android/MainActivity.kt +++ b/demo-app/src/main/kotlin/io/getstream/video/android/MainActivity.kt @@ -27,6 +27,7 @@ import io.getstream.video.android.compose.theme.VideoTheme import io.getstream.video.android.datastore.delegate.StreamUserDataStore import io.getstream.video.android.ui.AppNavHost import io.getstream.video.android.ui.AppScreens +import io.getstream.video.android.util.InAppUpdateHelper import io.getstream.video.android.util.InstallReferrer import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -34,9 +35,7 @@ import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { - @Inject lateinit var dataStore: StreamUserDataStore - private val firebaseAnalytics by lazy { FirebaseAnalytics.getInstance(this) } override fun onCreate(savedInstanceState: Bundle?) { @@ -66,6 +65,8 @@ class MainActivity : ComponentActivity() { ) } } + + InAppUpdateHelper(this@MainActivity).checkForAppUpdates() } } } diff --git a/demo-app/src/main/kotlin/io/getstream/video/android/util/InAppUpdateHelper.kt b/demo-app/src/main/kotlin/io/getstream/video/android/util/InAppUpdateHelper.kt new file mode 100644 index 0000000000..4ed69e7c23 --- /dev/null +++ b/demo-app/src/main/kotlin/io/getstream/video/android/util/InAppUpdateHelper.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * 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.getstream.video.android.util + +import android.util.Log +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.install.model.ActivityResult +import com.google.android.play.core.ktx.AppUpdateResult +import com.google.android.play.core.ktx.requestUpdateFlow +import io.getstream.video.android.R +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest + +class InAppUpdateHelper(private val activity: ComponentActivity) { + private lateinit var appUpdateResultFlow: Flow + private lateinit var appUpdateActivityResultLauncher: + ActivityResultLauncher + + init { + try { + appUpdateResultFlow = AppUpdateManagerFactory + .create(activity) + .requestUpdateFlow() + .catch { + emit(AppUpdateResult.NotAvailable) + } // Catch exception when app is not downloaded from Play Store + + appUpdateActivityResultLauncher = activity.registerForActivityResult( + ActivityResultContracts.StartIntentSenderForResult(), + ) { activityResult -> + handleAppUpdateActivityResult(activityResult.resultCode) + } + } catch (e: NullPointerException) { + Log.e( + IN_APP_UPDATE_LOG_TAG, + "Cannot initialize InAppUpdateHelper. Make sure it's instantiated in or after Activity.onCreate().", + ) + } + } + + suspend fun checkForAppUpdates() { + if (::appUpdateResultFlow.isInitialized) { + appUpdateResultFlow.collectLatest { result -> + when (result) { + is AppUpdateResult.Available -> { + Log.d(IN_APP_UPDATE_LOG_TAG, "Update available") + result.startImmediateUpdate(appUpdateActivityResultLauncher) + } + is AppUpdateResult.InProgress -> Log.d( + IN_APP_UPDATE_LOG_TAG, + "Update in progress", + ) + is AppUpdateResult.Downloaded -> Log.d( + IN_APP_UPDATE_LOG_TAG, + "Update downloaded", + ) + is AppUpdateResult.NotAvailable -> Log.i( + IN_APP_UPDATE_LOG_TAG, + "None available", + ) + } + } + } + } + + private fun handleAppUpdateActivityResult(resultCode: Int) { + when (resultCode) { + // For immediate updates, you might not receive RESULT_OK because + // the update should already be finished by the time control is given back to your app. + ComponentActivity.RESULT_OK -> { + Log.d(IN_APP_UPDATE_LOG_TAG, "Update successful") + showToast(activity.getString(R.string.in_app_update_successful)) + } + ComponentActivity.RESULT_CANCELED -> { + Log.d(IN_APP_UPDATE_LOG_TAG, "Update canceled") + showToast(activity.getString(R.string.in_app_update_canceled)) + } + ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> { + Log.d(IN_APP_UPDATE_LOG_TAG, "Update failed") + showToast(activity.getString(R.string.in_app_update_failed)) + } + } + } + + private fun showToast(userMessage: String) = + Toast.makeText(activity, userMessage, Toast.LENGTH_LONG).show() +} + +private const val IN_APP_UPDATE_LOG_TAG = "In-app update" diff --git a/demo-app/src/main/res/values/strings.xml b/demo-app/src/main/res/values/strings.xml index bf06a4fa6a..dd95b9776d 100644 --- a/demo-app/src/main/res/values/strings.xml +++ b/demo-app/src/main/res/values/strings.xml @@ -47,6 +47,9 @@ Restart App Error Log Log messages copied to clipboard! + App has been updated + Please consider installing the update.\nIt contains important features or bug fixes. + App update failed. Try again later %s is typing %s and %d more are typing diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad1cfeafa8..1e0c24535e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,6 +69,7 @@ firebaseCrashlytics = "2.9.5" installReferrer = "2.2" playCodeScanner = "16.1.0" +playAppUpdate = "2.1.0" hilt = "2.48.1" desugar = "2.0.3" @@ -187,6 +188,7 @@ firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics play-install-referrer = { group = "com.android.installreferrer", name = "installreferrer", version.ref = "installReferrer" } play-code-scanner = { group = "com.google.android.gms", name = "play-services-code-scanner", version.ref = "playCodeScanner" } +play-app-update-ktx = { group = "com.google.android.play", name = "app-update-ktx", version.ref = "playAppUpdate" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } leakCanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakCanary" }