Skip to content

Commit

Permalink
In-App update implemented with Remote config.
Browse files Browse the repository at this point in the history
  • Loading branch information
surinder-tsys committed Apr 23, 2024
1 parent f0e1e3b commit ec385b4
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 8 deletions.
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,10 @@ dependencies {
// upon each update first test: new registration, receive push
gplayImplementation "com.google.firebase:firebase-messaging:23.4.1"
gplayImplementation 'com.google.android.play:review-ktx:2.0.1'
// 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:21.4.1")

implementation 'com.github.nextcloud.android-common:ui:0.17.0'

Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.nmc.android.remoteconfig

/**
* class to fetch and activate remote config for the app update feature
*/
class RemoteConfigInit
166 changes: 166 additions & 0 deletions app/src/gplay/java/com/nmc/android/appupdate/InAppUpdateHelperImpl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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.get
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[FORCE_UPDATE_KEY].asBoolean()
private val appVersionCode = remoteConfig[APP_VERSION_KEY].asLong()

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<IntentSenderRequest> =
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()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.nmc.android.remoteconfig

import com.google.firebase.ktx.Firebase
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 {

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 val remoteConfig = Firebase.remoteConfig

init {
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.")
}
}
}
}
16 changes: 8 additions & 8 deletions app/src/gplay/res/values/setup.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
<!-- Push server url -->
<string name="push_server_url" translatable="false">https://push-notifications.nextcloud.com</string>

<string name="default_web_client_id" translatable="false">829118773643-cq33cmhv7mnv7iq8mjv6rt7t15afc70k.apps.googleusercontent.com</string>
<string name="firebase_database_url" translatable="false">https://nextcloud-a7dea.firebaseio.com</string>
<string name="gcm_defaultSenderId" translatable="false">829118773643</string>
<string name="google_api_key" translatable="false">AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s</string>
<string name="google_app_id" translatable="false">1:829118773643:android:512449826e931d0e</string>
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s</string>
<string name="google_storage_bucket" translatable="false">nextcloud-a7dea.appspot.com</string>
<string name="project_id" translatable="false">nextcloud-a7dea</string>
<string name="default_web_client_id" translatable="false">769898910423-mnfg2ntrfonapn4bu69q0j3mlgpqp4hl.apps.googleusercontent.com</string>
<string name="firebase_database_url" translatable="false">https://mediencenter-1099.firebaseio.com</string>
<string name="gcm_defaultSenderId" translatable="false">769898910423</string>
<string name="google_api_key" translatable="false">AIzaSyCbRA2hRNewmQyBfchT-cLzU0GqXnYpVwU</string>
<string name="google_app_id" translatable="false">1:769898910423:android:bf1c31423c5299ba</string>
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyCbRA2hRNewmQyBfchT-cLzU0GqXnYpVwU</string>
<string name="google_storage_bucket" translatable="false">mediencenter-1099.appspot.com</string>
<string name="project_id" translatable="false">mediencenter-1099</string>
</resources>


15 changes: 15 additions & 0 deletions app/src/main/java/com/nmc/android/appupdate/InAppUpdateHelper.kt
Original file line number Diff line number Diff line change
@@ -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()
}
4 changes: 4 additions & 0 deletions app/src/main/java/com/owncloud/android/MainApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,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;
Expand Down Expand Up @@ -355,6 +356,9 @@ public void onCreate() {

backgroundJobManager.schedulePeriodicHealthStatus();

// NMC Customization
new RemoteConfigInit();

registerGlobalPassCodeProtection();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -67,6 +71,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
Expand Down Expand Up @@ -298,4 +309,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;
}
}
2 changes: 2 additions & 0 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@
<string name="clipboard_no_text_to_copy">Kein Text zum Kopieren in die Zwischenablage empfangen</string>
<string name="clipboard_text_copied">Link kopiert</string>
<string name="clipboard_unexpected_error">Unerwarteter Fehler beim Kopieren in die Zwischenablage</string>
<string name="app_update_downloaded">Das Update wurde bereits heruntergeladen.</string>
<string name="common_restart">Neustart</string>
<string name="common_back">Zurück</string>
<string name="common_cancel">Abbrechen</string>
<string name="common_cancel_sync">Synchronisierung abbrechen</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@
<string name="common_skip">Skip</string>
<string name="common_copy">Copy</string>
<string name="about_title">About</string>
<string name="app_update_downloaded">An update has just been downloaded.</string>
<string name="common_restart">Restart</string>
<string name="remove_local_account">Remove local account</string>
<string name="remove_local_account_details">Remove account from device and delete all local files</string>
<string name="request_account_deletion">Request account deletion</string>
Expand Down
Loading

0 comments on commit ec385b4

Please sign in to comment.