Skip to content

Commit 39a1b0f

Browse files
committed
In-App update implemented with Remote config.
1 parent def5dba commit 39a1b0f

File tree

12 files changed

+313
-8
lines changed

12 files changed

+313
-8
lines changed

app/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ dependencies {
376376
// upon each update first test: new registration, receive push
377377
gplayImplementation "com.google.firebase:firebase-messaging:23.2.1"
378378
gplayImplementation 'com.google.android.play:review-ktx:2.0.1'
379+
// Kotlin extensions library for Play In-App Update ref: https://developer.android.com/guide/playcore/in-app-updates/kotlin-java#groovy
380+
gplayImplementation 'com.google.android.play:app-update-ktx:2.1.0'
381+
// firebase remote config
382+
gplayImplementation("com.google.firebase:firebase-config-ktx:21.4.1")
379383

380384
implementation 'com.github.nextcloud.android-common:ui:0.12.0'
381385

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.nmc.android.appupdate
2+
3+
import androidx.appcompat.app.AppCompatActivity
4+
5+
class InAppUpdateHelperImpl(private val activity: AppCompatActivity) : InAppUpdateHelper {
6+
7+
override fun onResume() {
8+
}
9+
10+
override fun onDestroy() {
11+
}
12+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.nmc.android.remoteconfig
2+
3+
/**
4+
* class to fetch and activate remote config for the app update feature
5+
*/
6+
class RemoteConfigInit
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package com.nmc.android.appupdate
2+
3+
import android.app.Activity
4+
import androidx.activity.result.ActivityResult
5+
import androidx.activity.result.ActivityResultLauncher
6+
import androidx.activity.result.IntentSenderRequest
7+
import androidx.activity.result.contract.ActivityResultContracts
8+
import androidx.appcompat.app.AppCompatActivity
9+
import com.google.android.material.snackbar.Snackbar
10+
import com.google.android.play.core.appupdate.AppUpdateInfo
11+
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
12+
import com.google.android.play.core.appupdate.AppUpdateOptions
13+
import com.google.android.play.core.install.InstallState
14+
import com.google.android.play.core.install.InstallStateUpdatedListener
15+
import com.google.android.play.core.install.model.ActivityResult.RESULT_IN_APP_UPDATE_FAILED
16+
import com.google.android.play.core.install.model.AppUpdateType
17+
import com.google.android.play.core.install.model.InstallStatus
18+
import com.google.android.play.core.install.model.UpdateAvailability
19+
import com.google.firebase.ktx.Firebase
20+
import com.google.firebase.remoteconfig.ktx.get
21+
import com.google.firebase.remoteconfig.ktx.remoteConfig
22+
import com.nmc.android.remoteconfig.RemoteConfigInit.Companion.APP_VERSION_KEY
23+
import com.nmc.android.remoteconfig.RemoteConfigInit.Companion.FORCE_UPDATE_KEY
24+
import com.owncloud.android.R
25+
import com.owncloud.android.lib.common.utils.Log_OC
26+
import com.owncloud.android.utils.DisplayUtils
27+
28+
class InAppUpdateHelperImpl(private val activity: AppCompatActivity) : InAppUpdateHelper, InstallStateUpdatedListener {
29+
30+
companion object {
31+
private val TAG = InAppUpdateHelperImpl::class.java.simpleName
32+
}
33+
34+
private val remoteConfig = Firebase.remoteConfig
35+
private val isForceUpdate = remoteConfig[FORCE_UPDATE_KEY].asBoolean()
36+
private val appVersionCode = remoteConfig[APP_VERSION_KEY].asLong()
37+
38+
private val appUpdateManager = AppUpdateManagerFactory.create(activity)
39+
40+
@AppUpdateType
41+
private var updateType = if (isForceUpdate) AppUpdateType.IMMEDIATE else AppUpdateType.FLEXIBLE
42+
43+
init {
44+
Log_OC.d(TAG, "App Update Remote Config Values : Force Update- $isForceUpdate -- Version Code- $appVersionCode")
45+
46+
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
47+
48+
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
49+
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
50+
Log_OC.d(TAG, "App update is available.")
51+
52+
// if app version in remote config is not equal to the latest app version code in play store
53+
// then do the flexible update instead of reading the value from remote config
54+
if (appUpdateInfo.availableVersionCode() != appVersionCode.toInt()) {
55+
Log_OC.d(
56+
TAG,
57+
"Available app version code mismatch with remote config. Setting update type to optional."
58+
)
59+
updateType = AppUpdateType.FLEXIBLE
60+
}
61+
62+
if (appUpdateInfo.isUpdateTypeAllowed(updateType)) {
63+
// Request the update.
64+
startAppUpdate(
65+
appUpdateInfo,
66+
updateType
67+
)
68+
}
69+
} else {
70+
Log_OC.d(TAG, "No app update available.")
71+
}
72+
}
73+
}
74+
75+
private fun startAppUpdate(
76+
appUpdateInfo: AppUpdateInfo,
77+
@AppUpdateType updateType: Int
78+
) {
79+
80+
if (updateType == AppUpdateType.FLEXIBLE) {
81+
// Before starting an update, register a listener for updates.
82+
appUpdateManager.registerListener(this)
83+
}
84+
85+
Log_OC.d(TAG, "App update dialog showing to the user.")
86+
87+
appUpdateManager.startUpdateFlowForResult(
88+
appUpdateInfo,
89+
appUpdateResultLauncher,
90+
AppUpdateOptions.newBuilder(updateType).build()
91+
)
92+
}
93+
94+
private val appUpdateResultLauncher: ActivityResultLauncher<IntentSenderRequest> =
95+
activity.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result: ActivityResult ->
96+
when (result.resultCode) {
97+
Activity.RESULT_OK -> {
98+
Log_OC.d(TAG, "The user has accepted to download the update or the download finished.")
99+
}
100+
101+
Activity.RESULT_CANCELED -> {
102+
Log_OC.e(TAG, "Update flow failed: The user has denied or canceled the update.")
103+
}
104+
105+
RESULT_IN_APP_UPDATE_FAILED -> {
106+
Log_OC.e(
107+
TAG,
108+
"Update flow failed: Some other error prevented either the user from providing consent or the update from proceeding."
109+
)
110+
}
111+
}
112+
113+
}
114+
115+
private fun flexibleUpdateDownloadCompleted() {
116+
DisplayUtils.createSnackbar(
117+
activity.findViewById(android.R.id.content),
118+
R.string.app_update_downloaded,
119+
Snackbar.LENGTH_INDEFINITE
120+
).apply {
121+
setAction(R.string.common_restart) { appUpdateManager.completeUpdate() }
122+
show()
123+
}
124+
}
125+
126+
override fun onResume() {
127+
appUpdateManager
128+
.appUpdateInfo
129+
.addOnSuccessListener { appUpdateInfo: AppUpdateInfo ->
130+
// for AppUpdateType.IMMEDIATE only, already executing updater
131+
if (updateType == AppUpdateType.IMMEDIATE) {
132+
if (appUpdateInfo.updateAvailability()
133+
== UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS
134+
) {
135+
Log_OC.d(TAG, "Resume the Immediate update if in-app update is already running.")
136+
// If an in-app update is already running, resume the update.
137+
startAppUpdate(
138+
appUpdateInfo,
139+
AppUpdateType.IMMEDIATE
140+
)
141+
}
142+
} else if (updateType == AppUpdateType.FLEXIBLE) {
143+
// If the update is downloaded but not installed,
144+
// notify the user to complete the update.
145+
if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) {
146+
Log_OC.d(TAG, "Resume: Flexible update is downloaded but not installed. User is notified.")
147+
flexibleUpdateDownloadCompleted()
148+
}
149+
}
150+
}
151+
}
152+
153+
override fun onDestroy() {
154+
appUpdateManager.unregisterListener(this)
155+
}
156+
157+
override fun onStateUpdate(state: InstallState) {
158+
if (state.installStatus() == InstallStatus.DOWNLOADED) {
159+
Log_OC.d(TAG, "Flexible update is downloaded. User is notified to restart the app.")
160+
161+
// After the update is downloaded, notifying user via snackbar
162+
// and request user confirmation to restart the app.
163+
flexibleUpdateDownloadCompleted()
164+
}
165+
}
166+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.nmc.android.remoteconfig
2+
3+
import com.google.firebase.ktx.Firebase
4+
import com.google.firebase.remoteconfig.ktx.remoteConfig
5+
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
6+
import com.owncloud.android.BuildConfig
7+
import com.owncloud.android.R
8+
import com.owncloud.android.lib.common.utils.Log_OC
9+
import java.util.concurrent.TimeUnit
10+
11+
/**
12+
* class to fetch and activate remote config for the app update feature
13+
*/
14+
class RemoteConfigInit {
15+
16+
companion object {
17+
private val TAG = RemoteConfigInit::class.java.simpleName
18+
19+
const val FORCE_UPDATE_KEY = "android_force_update"
20+
const val APP_VERSION_KEY = "android_app_version"
21+
22+
private const val INTERVAL_FOR_DEVELOPMENT = 0L //0 sec for immediate update
23+
24+
// by default the sync value is 12 hours which is not required in our case
25+
// as we will be only using this for app update and since the app updates are done in few months
26+
// so fetching the data in 1 day
27+
private val INTERVAL_FOR_PROD = TimeUnit.DAYS.toSeconds(1) //1 day
28+
29+
private fun getMinimumTimeToFetchConfigs(): Long {
30+
return if (BuildConfig.DEBUG) INTERVAL_FOR_DEVELOPMENT else INTERVAL_FOR_PROD
31+
}
32+
}
33+
34+
private val remoteConfig = Firebase.remoteConfig
35+
36+
init {
37+
val configSettings = remoteConfigSettings {
38+
minimumFetchIntervalInSeconds = getMinimumTimeToFetchConfigs()
39+
}
40+
remoteConfig.setConfigSettingsAsync(configSettings)
41+
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
42+
43+
fetchAndActivateConfigs()
44+
}
45+
46+
private fun fetchAndActivateConfigs() {
47+
remoteConfig.fetchAndActivate()
48+
.addOnCompleteListener { task ->
49+
if (task.isSuccessful) {
50+
val updated = task.result
51+
Log_OC.d(TAG, "Config params updated: $updated\nFetch and activate succeeded.")
52+
} else {
53+
Log_OC.e(TAG, "Fetch failed.")
54+
}
55+
}
56+
}
57+
}

app/src/gplay/res/values/setup.xml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
<!-- Push server url -->
44
<string name="push_server_url" translatable="false">https://push-notifications.nextcloud.com</string>
55

6-
<string name="default_web_client_id" translatable="false">829118773643-cq33cmhv7mnv7iq8mjv6rt7t15afc70k.apps.googleusercontent.com</string>
7-
<string name="firebase_database_url" translatable="false">https://nextcloud-a7dea.firebaseio.com</string>
8-
<string name="gcm_defaultSenderId" translatable="false">829118773643</string>
9-
<string name="google_api_key" translatable="false">AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s</string>
10-
<string name="google_app_id" translatable="false">1:829118773643:android:512449826e931d0e</string>
11-
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s</string>
12-
<string name="google_storage_bucket" translatable="false">nextcloud-a7dea.appspot.com</string>
13-
<string name="project_id" translatable="false">nextcloud-a7dea</string>
6+
<string name="default_web_client_id" translatable="false">769898910423-mnfg2ntrfonapn4bu69q0j3mlgpqp4hl.apps.googleusercontent.com</string>
7+
<string name="firebase_database_url" translatable="false">https://mediencenter-1099.firebaseio.com</string>
8+
<string name="gcm_defaultSenderId" translatable="false">769898910423</string>
9+
<string name="google_api_key" translatable="false">AIzaSyCbRA2hRNewmQyBfchT-cLzU0GqXnYpVwU</string>
10+
<string name="google_app_id" translatable="false">1:769898910423:android:bf1c31423c5299ba</string>
11+
<string name="google_crash_reporting_api_key" translatable="false">AIzaSyCbRA2hRNewmQyBfchT-cLzU0GqXnYpVwU</string>
12+
<string name="google_storage_bucket" translatable="false">mediencenter-1099.appspot.com</string>
13+
<string name="project_id" translatable="false">mediencenter-1099</string>
1414
</resources>
1515

1616

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.nmc.android.appupdate
2+
3+
interface InAppUpdateHelper {
4+
/**
5+
* function should be called from activity onResume
6+
* to check if the update is downloaded or still in progress
7+
*/
8+
fun onResume()
9+
10+
/**
11+
* function should be called from activity onDestroy
12+
* this will unregister the update listener attached for Flexible update
13+
*/
14+
fun onDestroy()
15+
}

app/src/main/java/com/owncloud/android/MainApp.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
import com.owncloud.android.lib.resources.status.NextcloudVersion;
8282
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
8383
import com.owncloud.android.ui.activity.SyncedFoldersActivity;
84+
import com.nmc.android.remoteconfig.RemoteConfigInit;
8485
import com.owncloud.android.ui.notifications.NotificationUtils;
8586
import com.owncloud.android.utils.DisplayUtils;
8687
import com.owncloud.android.utils.FilesSyncHelper;
@@ -346,6 +347,9 @@ public void onCreate() {
346347
backgroundJobManager.scheduleMediaFoldersDetectionJob();
347348
backgroundJobManager.startMediaFoldersDetectionJob();
348349

350+
// NMC Customization
351+
new RemoteConfigInit();
352+
349353
registerGlobalPassCodeProtection();
350354
}
351355

app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import android.graphics.Bitmap;
3030
import android.graphics.Color;
3131
import android.graphics.drawable.Drawable;
32+
import android.os.Bundle;
3233
import android.view.View;
3334
import android.widget.FrameLayout;
3435
import android.widget.ImageView;
@@ -43,6 +44,8 @@
4344
import com.google.android.material.textview.MaterialTextView;
4445
import com.nextcloud.client.di.Injectable;
4546
import com.owncloud.android.R;
47+
import com.nmc.android.appupdate.InAppUpdateHelper;
48+
import com.nmc.android.appupdate.InAppUpdateHelperImpl;
4649
import com.owncloud.android.datamodel.FileDataStorageManager;
4750
import com.owncloud.android.datamodel.OCFile;
4851
import com.owncloud.android.utils.theme.ThemeColorUtils;
@@ -52,6 +55,7 @@
5255
import javax.inject.Inject;
5356

5457
import androidx.annotation.NonNull;
58+
import androidx.annotation.Nullable;
5559
import androidx.annotation.StringRes;
5660
import androidx.annotation.VisibleForTesting;
5761
import androidx.appcompat.app.ActionBar;
@@ -79,6 +83,13 @@ public abstract class ToolbarActivity extends BaseActivity implements Injectable
7983
@Inject public ThemeColorUtils themeColorUtils;
8084
@Inject public ThemeUtils themeUtils;
8185
@Inject public ViewThemeUtils viewThemeUtils;
86+
private InAppUpdateHelper inAppUpdateHelper;
87+
88+
@Override
89+
protected void onCreate(@Nullable Bundle savedInstanceState) {
90+
super.onCreate(savedInstanceState);
91+
inAppUpdateHelper = new InAppUpdateHelperImpl(this);
92+
}
8293

8394
/**
8495
* Toolbar setup that must be called in implementer's {@link #onCreate} after {@link #setContentView} if they want
@@ -293,4 +304,19 @@ public void clearToolbarSubtitle() {
293304
actionBar.setSubtitle(null);
294305
}
295306
}
307+
308+
@Override
309+
protected void onResume() {
310+
super.onResume();
311+
// Checks that the update is not stalled during 'onResume()'.
312+
// However, you should execute this check at all entry points into the app.
313+
inAppUpdateHelper.onResume();
314+
}
315+
316+
@Override
317+
protected void onDestroy() {
318+
super.onDestroy();
319+
inAppUpdateHelper.onDestroy();
320+
inAppUpdateHelper = null;
321+
}
296322
}

app/src/main/res/values-de/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@
104104
<string name="clipboard_no_text_to_copy">Kein Text zum Kopieren in die Zwischenablage empfangen</string>
105105
<string name="clipboard_text_copied">Link kopiert</string>
106106
<string name="clipboard_unexpected_error">Unerwarteter Fehler beim Kopieren in die Zwischenablage</string>
107+
<string name="app_update_downloaded">Das Update wurde bereits heruntergeladen.</string>
108+
<string name="common_restart">Neustart</string>
107109
<string name="common_back">Zurück</string>
108110
<string name="common_cancel">Abbrechen</string>
109111
<string name="common_cancel_sync">Synchronisierung abbrechen</string>

0 commit comments

Comments
 (0)