Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable-3.29] Nmc/2574 In-App update funcationality #193

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,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 @@ -130,6 +130,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