From c05dd66566faf0780958bd62e43e0d437b1ed889 Mon Sep 17 00:00:00 2001 From: A117870935 Date: Wed, 18 Oct 2023 13:05:48 +0530 Subject: [PATCH 1/2] In-App update implemented with Remote config. --- app/build.gradle | 7 + .../InAppUpdateHelperImpl.kt | 12 ++ .../android/remoteconfig/RemoteConfigInit.kt | 6 + .../appupdate/InAppUpdateHelperImpl.kt | 165 ++++++++++++++++++ .../android/remoteconfig/RemoteConfigInit.kt | 70 ++++++++ app/src/gplay/res/values/setup.xml | 16 +- .../android/appupdate/InAppUpdateHelper.kt | 15 ++ .../java/com/owncloud/android/MainApp.java | 4 + .../android/ui/activity/ToolbarActivity.java | 26 +++ .../res/values-de/nmc_app_update_strings.xml | 5 + .../res/values/nmc_app_update_strings.xml | 5 + .../main/res/xml/remote_config_defaults.xml | 11 ++ nmc_app_update-dependencies.gradle | 12 ++ 13 files changed, 346 insertions(+), 8 deletions(-) create mode 100644 app/src/generic/java/com/nmc.android.appupdate/InAppUpdateHelperImpl.kt create mode 100644 app/src/generic/java/com/nmc/android/remoteconfig/RemoteConfigInit.kt create mode 100644 app/src/gplay/java/com/nmc/android/appupdate/InAppUpdateHelperImpl.kt create mode 100644 app/src/gplay/java/com/nmc/android/remoteconfig/RemoteConfigInit.kt create mode 100644 app/src/main/java/com/nmc/android/appupdate/InAppUpdateHelper.kt create mode 100644 app/src/main/res/values-de/nmc_app_update_strings.xml create mode 100644 app/src/main/res/values/nmc_app_update_strings.xml create mode 100644 app/src/main/res/xml/remote_config_defaults.xml create mode 100644 nmc_app_update-dependencies.gradle diff --git a/app/build.gradle b/app/build.gradle index cf23fbc85a92..4a11d172e0b8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,6 +49,8 @@ apply plugin: "io.gitlab.arturbosch.detekt" if (shotTest) { apply plugin: "shot" } +// apply In-App Update SDK for NMC +apply from: "$rootProject.projectDir/nmc_app_update-dependencies.gradle" apply plugin: "com.google.devtools.ksp" @@ -431,6 +433,11 @@ dependencies { implementation "com.github.nextcloud.android-common:ui:$androidCommonLibraryVersion" + // apply the dependencies from nmc_auto_update-dependencies.gradle + // as auto update dependencies need to be included only in gplay variant + // this is the way to do this + project.ext.addDependencies(project) + implementation "io.coil-kt:coil:2.7.0" } diff --git a/app/src/generic/java/com/nmc.android.appupdate/InAppUpdateHelperImpl.kt b/app/src/generic/java/com/nmc.android.appupdate/InAppUpdateHelperImpl.kt new file mode 100644 index 000000000000..647402e6dd57 --- /dev/null +++ b/app/src/generic/java/com/nmc.android.appupdate/InAppUpdateHelperImpl.kt @@ -0,0 +1,12 @@ +package com.nmc.android.appupdate + +import androidx.appcompat.app.AppCompatActivity + +class InAppUpdateHelperImpl(private val activity: AppCompatActivity) : InAppUpdateHelper { + + override fun onResume() { + } + + override fun onDestroy() { + } +} \ No newline at end of file diff --git a/app/src/generic/java/com/nmc/android/remoteconfig/RemoteConfigInit.kt b/app/src/generic/java/com/nmc/android/remoteconfig/RemoteConfigInit.kt new file mode 100644 index 000000000000..c4627249fa87 --- /dev/null +++ b/app/src/generic/java/com/nmc/android/remoteconfig/RemoteConfigInit.kt @@ -0,0 +1,6 @@ +package com.nmc.android.remoteconfig + +/** + * class to fetch and activate remote config for the app update feature + */ +class RemoteConfigInit \ No newline at end of file diff --git a/app/src/gplay/java/com/nmc/android/appupdate/InAppUpdateHelperImpl.kt b/app/src/gplay/java/com/nmc/android/appupdate/InAppUpdateHelperImpl.kt new file mode 100644 index 000000000000..6f15e24a2659 --- /dev/null +++ b/app/src/gplay/java/com/nmc/android/appupdate/InAppUpdateHelperImpl.kt @@ -0,0 +1,165 @@ +package com.nmc.android.appupdate + +import android.app.Activity +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +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.appupdate.AppUpdateOptions +import com.google.android.play.core.install.InstallState +import com.google.android.play.core.install.InstallStateUpdatedListener +import com.google.android.play.core.install.model.ActivityResult.RESULT_IN_APP_UPDATE_FAILED +import com.google.android.play.core.install.model.AppUpdateType +import com.google.android.play.core.install.model.InstallStatus +import com.google.android.play.core.install.model.UpdateAvailability +import com.google.firebase.ktx.Firebase +import com.google.firebase.remoteconfig.ktx.remoteConfig +import com.nmc.android.remoteconfig.RemoteConfigInit.Companion.APP_VERSION_KEY +import com.nmc.android.remoteconfig.RemoteConfigInit.Companion.FORCE_UPDATE_KEY +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.DisplayUtils + +class InAppUpdateHelperImpl(private val activity: AppCompatActivity) : InAppUpdateHelper, InstallStateUpdatedListener { + + companion object { + private val TAG = InAppUpdateHelperImpl::class.java.simpleName + } + + private val remoteConfig = Firebase.remoteConfig + private val isForceUpdate = remoteConfig.getBoolean(FORCE_UPDATE_KEY) + private val appVersionCode = remoteConfig.getLong(APP_VERSION_KEY) + + private val appUpdateManager = AppUpdateManagerFactory.create(activity) + + @AppUpdateType + private var updateType = if (isForceUpdate) AppUpdateType.IMMEDIATE else AppUpdateType.FLEXIBLE + + init { + Log_OC.d(TAG, "App Update Remote Config Values : Force Update- $isForceUpdate -- Version Code- $appVersionCode") + + val appUpdateInfoTask = appUpdateManager.appUpdateInfo + + appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> + if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) { + Log_OC.d(TAG, "App update is available.") + + // if app version in remote config is not equal to the latest app version code in play store + // then do the flexible update instead of reading the value from remote config + if (appUpdateInfo.availableVersionCode() != appVersionCode.toInt()) { + Log_OC.d( + TAG, + "Available app version code mismatch with remote config. Setting update type to optional." + ) + updateType = AppUpdateType.FLEXIBLE + } + + if (appUpdateInfo.isUpdateTypeAllowed(updateType)) { + // Request the update. + startAppUpdate( + appUpdateInfo, + updateType + ) + } + } else { + Log_OC.d(TAG, "No app update available.") + } + } + } + + private fun startAppUpdate( + appUpdateInfo: AppUpdateInfo, + @AppUpdateType updateType: Int + ) { + + if (updateType == AppUpdateType.FLEXIBLE) { + // Before starting an update, register a listener for updates. + appUpdateManager.registerListener(this) + } + + Log_OC.d(TAG, "App update dialog showing to the user.") + + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + appUpdateResultLauncher, + AppUpdateOptions.newBuilder(updateType).build() + ) + } + + private val appUpdateResultLauncher: ActivityResultLauncher = + activity.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result: ActivityResult -> + when (result.resultCode) { + Activity.RESULT_OK -> { + Log_OC.d(TAG, "The user has accepted to download the update or the download finished.") + } + + Activity.RESULT_CANCELED -> { + Log_OC.e(TAG, "Update flow failed: The user has denied or canceled the update.") + } + + RESULT_IN_APP_UPDATE_FAILED -> { + Log_OC.e( + TAG, + "Update flow failed: Some other error prevented either the user from providing consent or the update from proceeding." + ) + } + } + + } + + private fun flexibleUpdateDownloadCompleted() { + DisplayUtils.createSnackbar( + activity.findViewById(android.R.id.content), + R.string.app_update_downloaded, + Snackbar.LENGTH_INDEFINITE + ).apply { + setAction(R.string.common_restart) { appUpdateManager.completeUpdate() } + show() + } + } + + override fun onResume() { + appUpdateManager + .appUpdateInfo + .addOnSuccessListener { appUpdateInfo: AppUpdateInfo -> + // for AppUpdateType.IMMEDIATE only, already executing updater + if (updateType == AppUpdateType.IMMEDIATE) { + if (appUpdateInfo.updateAvailability() + == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS + ) { + Log_OC.d(TAG, "Resume the Immediate update if in-app update is already running.") + // If an in-app update is already running, resume the update. + startAppUpdate( + appUpdateInfo, + AppUpdateType.IMMEDIATE + ) + } + } else if (updateType == AppUpdateType.FLEXIBLE) { + // If the update is downloaded but not installed, + // notify the user to complete the update. + if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) { + Log_OC.d(TAG, "Resume: Flexible update is downloaded but not installed. User is notified.") + flexibleUpdateDownloadCompleted() + } + } + } + } + + override fun onDestroy() { + appUpdateManager.unregisterListener(this) + } + + override fun onStateUpdate(state: InstallState) { + if (state.installStatus() == InstallStatus.DOWNLOADED) { + Log_OC.d(TAG, "Flexible update is downloaded. User is notified to restart the app.") + + // After the update is downloaded, notifying user via snackbar + // and request user confirmation to restart the app. + flexibleUpdateDownloadCompleted() + } + } +} \ No newline at end of file diff --git a/app/src/gplay/java/com/nmc/android/remoteconfig/RemoteConfigInit.kt b/app/src/gplay/java/com/nmc/android/remoteconfig/RemoteConfigInit.kt new file mode 100644 index 000000000000..1b977e9c0a03 --- /dev/null +++ b/app/src/gplay/java/com/nmc/android/remoteconfig/RemoteConfigInit.kt @@ -0,0 +1,70 @@ +package com.nmc.android.remoteconfig + +import android.app.Application +import com.google.firebase.FirebaseApp +import com.google.firebase.ktx.Firebase +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.ktx.remoteConfig +import com.google.firebase.remoteconfig.ktx.remoteConfigSettings +import com.owncloud.android.BuildConfig +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import java.util.concurrent.TimeUnit + +/** + * class to fetch and activate remote config for the app update feature + */ +class RemoteConfigInit(context: Application) { + + companion object { + private val TAG = RemoteConfigInit::class.java.simpleName + + const val FORCE_UPDATE_KEY = "android_force_update" + const val APP_VERSION_KEY = "android_app_version" + + private const val INTERVAL_FOR_DEVELOPMENT = 0L //0 sec for immediate update + + // by default the sync value is 12 hours which is not required in our case + // as we will be only using this for app update and since the app updates are done in few months + // so fetching the data in 1 day + private val INTERVAL_FOR_PROD = TimeUnit.DAYS.toSeconds(1) //1 day + + private fun getMinimumTimeToFetchConfigs(): Long { + return if (BuildConfig.DEBUG) INTERVAL_FOR_DEVELOPMENT else INTERVAL_FOR_PROD + } + } + + private var remoteConfig: FirebaseRemoteConfig + + init { + // init firebase + // fix: NMC-3449 + // fix: NMC-3848 + // Initialize Firebase (only if not already initialized) + if (FirebaseApp.getApps(context).isEmpty()) { + FirebaseApp.initializeApp(context) + } + + remoteConfig = Firebase.remoteConfig + + val configSettings = remoteConfigSettings { + minimumFetchIntervalInSeconds = getMinimumTimeToFetchConfigs() + } + remoteConfig.setConfigSettingsAsync(configSettings) + remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) + + fetchAndActivateConfigs() + } + + private fun fetchAndActivateConfigs() { + remoteConfig.fetchAndActivate() + .addOnCompleteListener { task -> + if (task.isSuccessful) { + val updated = task.result + Log_OC.d(TAG, "Config params updated: $updated\nFetch and activate succeeded.") + } else { + Log_OC.e(TAG, "Fetch failed.") + } + } + } +} \ No newline at end of file diff --git a/app/src/gplay/res/values/setup.xml b/app/src/gplay/res/values/setup.xml index 83fadc4e35d1..368484b9ccbe 100644 --- a/app/src/gplay/res/values/setup.xml +++ b/app/src/gplay/res/values/setup.xml @@ -9,14 +9,14 @@ https://push-notifications.nextcloud.com - 829118773643-cq33cmhv7mnv7iq8mjv6rt7t15afc70k.apps.googleusercontent.com - https://nextcloud-a7dea.firebaseio.com - 829118773643 - AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s - 1:829118773643:android:512449826e931d0e - AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s - nextcloud-a7dea.appspot.com - nextcloud-a7dea + 769898910423-mnfg2ntrfonapn4bu69q0j3mlgpqp4hl.apps.googleusercontent.com + https://mediencenter-1099.firebaseio.com + 769898910423 + AIzaSyCbRA2hRNewmQyBfchT-cLzU0GqXnYpVwU + 1:769898910423:android:bf1c31423c5299ba + AIzaSyCbRA2hRNewmQyBfchT-cLzU0GqXnYpVwU + mediencenter-1099.appspot.com + mediencenter-1099 diff --git a/app/src/main/java/com/nmc/android/appupdate/InAppUpdateHelper.kt b/app/src/main/java/com/nmc/android/appupdate/InAppUpdateHelper.kt new file mode 100644 index 000000000000..c4951ae6b81f --- /dev/null +++ b/app/src/main/java/com/nmc/android/appupdate/InAppUpdateHelper.kt @@ -0,0 +1,15 @@ +package com.nmc.android.appupdate + +interface InAppUpdateHelper { + /** + * function should be called from activity onResume + * to check if the update is downloaded or still in progress + */ + fun onResume() + + /** + * function should be called from activity onDestroy + * this will unregister the update listener attached for Flexible update + */ + fun onDestroy() +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 6db2650e3e5c..26a45b284e82 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -82,6 +82,7 @@ import com.owncloud.android.lib.resources.status.NextcloudVersion; import com.owncloud.android.lib.resources.status.OwnCloudVersion; import com.owncloud.android.ui.activity.SyncedFoldersActivity; +import com.nmc.android.remoteconfig.RemoteConfigInit; import com.owncloud.android.ui.notifications.NotificationUtils; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.FilesSyncHelper; @@ -380,6 +381,9 @@ public void onCreate() { backgroundJobManager.startPeriodicallyOfflineOperation(); } + // NMC Customization + new RemoteConfigInit(this); + registerGlobalPassCodeProtection(); networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService); registerNetworkChangeReceiver(); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java index 0e56749caa79..22f2c23c1921 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java @@ -18,6 +18,7 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.Drawable; +import android.os.Bundle; import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; @@ -32,6 +33,8 @@ import com.google.android.material.textview.MaterialTextView; import com.nextcloud.client.di.Injectable; import com.owncloud.android.R; +import com.nmc.android.appupdate.InAppUpdateHelper; +import com.nmc.android.appupdate.InAppUpdateHelperImpl; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.utils.theme.ThemeColorUtils; @@ -41,6 +44,7 @@ import javax.inject.Inject; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.AppCompatSpinner; @@ -69,6 +73,13 @@ public abstract class ToolbarActivity extends BaseActivity implements Injectable @Inject public ThemeColorUtils themeColorUtils; @Inject public ThemeUtils themeUtils; @Inject public ViewThemeUtils viewThemeUtils; + private InAppUpdateHelper inAppUpdateHelper; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + inAppUpdateHelper = new InAppUpdateHelperImpl(this); + } /** * Toolbar setup that must be called in implementer's {@link #onCreate} after {@link #setContentView} if they want @@ -309,4 +320,19 @@ public void clearToolbarSubtitle() { actionBar.setSubtitle(null); } } + + @Override + protected void onResume() { + super.onResume(); + // Checks that the update is not stalled during 'onResume()'. + // However, you should execute this check at all entry points into the app. + inAppUpdateHelper.onResume(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + inAppUpdateHelper.onDestroy(); + inAppUpdateHelper = null; + } } diff --git a/app/src/main/res/values-de/nmc_app_update_strings.xml b/app/src/main/res/values-de/nmc_app_update_strings.xml new file mode 100644 index 000000000000..4d5a232c8d0f --- /dev/null +++ b/app/src/main/res/values-de/nmc_app_update_strings.xml @@ -0,0 +1,5 @@ + + + Das Update wurde bereits heruntergeladen. + Neustart + \ No newline at end of file diff --git a/app/src/main/res/values/nmc_app_update_strings.xml b/app/src/main/res/values/nmc_app_update_strings.xml new file mode 100644 index 000000000000..1339cd1f3526 --- /dev/null +++ b/app/src/main/res/values/nmc_app_update_strings.xml @@ -0,0 +1,5 @@ + + + An update has just been downloaded. + Restart + \ No newline at end of file diff --git a/app/src/main/res/xml/remote_config_defaults.xml b/app/src/main/res/xml/remote_config_defaults.xml new file mode 100644 index 000000000000..eab9b406e38e --- /dev/null +++ b/app/src/main/res/xml/remote_config_defaults.xml @@ -0,0 +1,11 @@ + + + + android_force_update + false + + + android_app_version + 72123 + + \ No newline at end of file diff --git a/nmc_app_update-dependencies.gradle b/nmc_app_update-dependencies.gradle new file mode 100644 index 000000000000..b0fae225c150 --- /dev/null +++ b/nmc_app_update-dependencies.gradle @@ -0,0 +1,12 @@ +// way to add auto update dependencies to only gplay variant +ext { + addDependencies = { + project -> + project.dependencies { + // Kotlin extensions library for Play In-App Update ref: https://developer.android.com/guide/playcore/in-app-updates/kotlin-java#groovy + gplayImplementation 'com.google.android.play:app-update-ktx:2.1.0' + // firebase remote config + gplayImplementation "com.google.firebase:firebase-config-ktx:22.0.0" + } + } +} \ No newline at end of file From 627b34a5e56e7993d9777e579a96a8b7703a7260 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Tue, 12 Aug 2025 07:28:08 +0200 Subject: [PATCH 2/2] setup.xml changes no longer needed, since done via brander --- app/src/gplay/res/values/setup.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/gplay/res/values/setup.xml b/app/src/gplay/res/values/setup.xml index 368484b9ccbe..83fadc4e35d1 100644 --- a/app/src/gplay/res/values/setup.xml +++ b/app/src/gplay/res/values/setup.xml @@ -9,14 +9,14 @@ https://push-notifications.nextcloud.com - 769898910423-mnfg2ntrfonapn4bu69q0j3mlgpqp4hl.apps.googleusercontent.com - https://mediencenter-1099.firebaseio.com - 769898910423 - AIzaSyCbRA2hRNewmQyBfchT-cLzU0GqXnYpVwU - 1:769898910423:android:bf1c31423c5299ba - AIzaSyCbRA2hRNewmQyBfchT-cLzU0GqXnYpVwU - mediencenter-1099.appspot.com - mediencenter-1099 + 829118773643-cq33cmhv7mnv7iq8mjv6rt7t15afc70k.apps.googleusercontent.com + https://nextcloud-a7dea.firebaseio.com + 829118773643 + AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s + 1:829118773643:android:512449826e931d0e + AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s + nextcloud-a7dea.appspot.com + nextcloud-a7dea