diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 9e3d56af9f2d..9a94a59d41d6 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -166,6 +166,7 @@ android { buildConfigField "boolean", "DASHBOARD_PERSONALIZATION", "false" buildConfigField "boolean", "ENABLE_SITE_MONITORING", "false" buildConfigField "boolean", "SYNC_PUBLISHING", "false" + buildConfigField "boolean", "ENABLE_IN_APP_UPDATES", "false" manifestPlaceholders = [magicLinkScheme:"wordpress"] } @@ -391,6 +392,9 @@ dependencies { implementation "org.wordpress:persistentedittext:$wordPressPersistentEditTextVersion" implementation "$gradle.ext.gravatarBinaryPath:$gravatarVersion" + implementation "com.google.android.play:app-update:$googlePlayInAppUpdateVersion" + implementation "com.google.android.play:app-update-ktx:$googlePlayInAppUpdateVersion" + implementation "androidx.arch.core:core-common:$androidxArchCoreVersion" implementation "androidx.arch.core:core-runtime:$androidxArchCoreVersion" implementation "com.google.code.gson:gson:$googleGsonVersion" diff --git a/WordPress/src/jetpack/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfigConstants.kt b/WordPress/src/jetpack/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfigConstants.kt new file mode 100644 index 000000000000..3d539360338c --- /dev/null +++ b/WordPress/src/jetpack/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfigConstants.kt @@ -0,0 +1,5 @@ +package org.wordpress.android.util.config + +const val IN_APP_UPDATE_BLOCKING_VERSION_REMOTE_FIELD = "jp_in_app_update_blocking_version_android" + + diff --git a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt index 2ad4a34dbaa2..bfe756467ab2 100644 --- a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt +++ b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt @@ -286,7 +286,6 @@ class AppInitializer @Inject constructor( crashLogging.initialize() dispatcher.register(this) appConfig.init(appScope) - // Upload any encrypted logs that were queued but not yet uploaded encryptedLogging.start() diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/IInAppUpdateManager.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/IInAppUpdateManager.kt new file mode 100644 index 000000000000..fc1135f274c8 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/IInAppUpdateManager.kt @@ -0,0 +1,15 @@ +package org.wordpress.android.inappupdate + +import android.app.Activity + +interface IInAppUpdateManager { + fun checkForAppUpdate(activity: Activity, listener: InAppUpdateListener) + fun completeAppUpdate() + fun cancelAppUpdate(updateType: Int) + fun onUserAcceptedAppUpdate(updateType: Int) + + companion object { + const val APP_UPDATE_IMMEDIATE_REQUEST_CODE = 1001 + const val APP_UPDATE_FLEXIBLE_REQUEST_CODE = 1002 + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt new file mode 100644 index 000000000000..abf15b094bfd --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTracker.kt @@ -0,0 +1,40 @@ +package org.wordpress.android.inappupdate + +import com.google.android.play.core.install.model.AppUpdateType +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import javax.inject.Inject + +class InAppUpdateAnalyticsTracker @Inject constructor( + private val tracker: AnalyticsTrackerWrapper +) { + fun trackUpdateShown(updateType: Int) { + tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_SHOWN, createPropertyMap(updateType)) + } + + fun trackUpdateAccepted(updateType: Int) { + tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_ACCEPTED, createPropertyMap(updateType)) + } + + fun trackUpdateDismissed(updateType: Int) { + tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_DISMISSED, createPropertyMap(updateType)) + } + + fun trackAppRestartToCompleteUpdate() { + tracker.track(AnalyticsTracker.Stat.IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART) + } + + private fun createPropertyMap(updateType: Int): Map { + return when (updateType) { + AppUpdateType.FLEXIBLE -> mapOf(PROPERTY_UPDATE_TYPE to UPDATE_TYPE_FLEXIBLE) + AppUpdateType.IMMEDIATE -> mapOf(PROPERTY_UPDATE_TYPE to UPDATE_TYPE_BLOCKING) + else -> emptyMap() + } + } + + companion object { + const val PROPERTY_UPDATE_TYPE = "type" + const val UPDATE_TYPE_FLEXIBLE = "flexible" + const val UPDATE_TYPE_BLOCKING = "blocking" + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateListener.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateListener.kt new file mode 100644 index 000000000000..e002395f3cd9 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateListener.kt @@ -0,0 +1,34 @@ +package org.wordpress.android.inappupdate + +/** + * Abstract class for handling callbacks related to in-app update events. + * + * Each method provides a default implementation that does nothing, allowing + * implementers to only override the necessary methods without implementing + * all callback methods. + */ +abstract class InAppUpdateListener { + open fun onAppUpdateStarted(type: Int) { + // Default empty implementation + } + + open fun onAppUpdateDownloaded() { + // Default empty implementation + } + + open fun onAppUpdateInstalled() { + // Default empty implementation + } + + open fun onAppUpdateFailed() { + // Default empty implementation + } + + open fun onAppUpdateCancelled() { + // Default empty implementation + } + + open fun onAppUpdatePending() { + // Default empty implementation + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt new file mode 100644 index 000000000000..11ac7fc132e1 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerImpl.kt @@ -0,0 +1,230 @@ +package org.wordpress.android.inappupdate + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.util.Log +import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateManager +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.AppUpdateType +import com.google.android.play.core.install.model.InstallStatus +import com.google.android.play.core.install.model.InstallStatus.CANCELED +import com.google.android.play.core.install.model.InstallStatus.DOWNLOADED +import com.google.android.play.core.install.model.InstallStatus.DOWNLOADING +import com.google.android.play.core.install.model.InstallStatus.FAILED +import com.google.android.play.core.install.model.InstallStatus.INSTALLED +import com.google.android.play.core.install.model.InstallStatus.INSTALLING +import com.google.android.play.core.install.model.InstallStatus.PENDING +import com.google.android.play.core.install.model.UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS +import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_AVAILABLE +import com.google.android.play.core.install.model.UpdateAvailability.UPDATE_NOT_AVAILABLE +import dagger.hilt.android.qualifiers.ApplicationContext +import org.wordpress.android.inappupdate.IInAppUpdateManager.Companion.APP_UPDATE_FLEXIBLE_REQUEST_CODE +import org.wordpress.android.inappupdate.IInAppUpdateManager.Companion.APP_UPDATE_IMMEDIATE_REQUEST_CODE + +import org.wordpress.android.util.BuildConfigWrapper +import org.wordpress.android.util.config.RemoteConfigWrapper +import javax.inject.Singleton + +@Singleton +@Suppress("TooManyFunctions") +class InAppUpdateManagerImpl( + @ApplicationContext private val applicationContext: Context, + private val appUpdateManager: AppUpdateManager, + private val remoteConfigWrapper: RemoteConfigWrapper, + private val buildConfigWrapper: BuildConfigWrapper, + private val inAppUpdateAnalyticsTracker: InAppUpdateAnalyticsTracker, + private val currentTimeProvider: () -> Long = {System.currentTimeMillis()} +): IInAppUpdateManager { + private var updateListener: InAppUpdateListener? = null + + override fun checkForAppUpdate(activity: Activity, listener: InAppUpdateListener) { + updateListener = listener + appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo -> + handleUpdateInfoSuccess(appUpdateInfo, activity) + }.addOnFailureListener { exception -> + Log.e(TAG, "Failed to check for update: ${exception.message}") + } + } + + override fun completeAppUpdate() { + inAppUpdateAnalyticsTracker.trackAppRestartToCompleteUpdate() + appUpdateManager.completeUpdate() + } + + override fun cancelAppUpdate(updateType: Int) { + appUpdateManager.unregisterListener(installStateListener) + inAppUpdateAnalyticsTracker.trackUpdateDismissed(updateType) + } + + override fun onUserAcceptedAppUpdate(updateType: Int) { + inAppUpdateAnalyticsTracker.trackUpdateAccepted(updateType) + } + + private fun handleUpdateInfoSuccess(appUpdateInfo: AppUpdateInfo, activity: Activity) { + when (appUpdateInfo.updateAvailability()) { + UPDATE_NOT_AVAILABLE -> { + /* do nothing */ + } + UPDATE_AVAILABLE -> { + handleUpdateAvailable(appUpdateInfo, activity) + } + DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS -> { + handleUpdateInProgress(appUpdateInfo, activity) + } + else -> { /* do nothing */ } + } + } + + private fun handleUpdateAvailable(appUpdateInfo: AppUpdateInfo, activity: Activity) { + if (appUpdateInfo.installStatus() == DOWNLOADED) { + updateListener?.onAppUpdateDownloaded() + return + } + + val updateVersion = getAvailableUpdateAppVersion(appUpdateInfo) + if (updateVersion != getLastUpdateRequestedVersion()) { + resetLastUpdateRequestInfo() + } + + if (isImmediateUpdateNecessary()) { + if (shouldRequestImmediateUpdate()) { + requestImmediateUpdate(appUpdateInfo, activity) + } + } else if (shouldRequestFlexibleUpdate()) { + requestFlexibleUpdate(appUpdateInfo, activity) + } + } + + private fun handleUpdateInProgress(appUpdateInfo: AppUpdateInfo, activity: Activity) { + if (isImmediateUpdateInProgress(appUpdateInfo)) { + requestImmediateUpdate(appUpdateInfo, activity) + } else { + requestFlexibleUpdate(appUpdateInfo, activity) + } + } + + private fun requestImmediateUpdate(appUpdateInfo: AppUpdateInfo, activity: Activity) { + updateListener?.onAppUpdateStarted(AppUpdateType.IMMEDIATE) + requestUpdate(AppUpdateType.IMMEDIATE, appUpdateInfo, activity) + } + + private fun requestFlexibleUpdate(appUpdateInfo: AppUpdateInfo, activity: Activity) { + appUpdateManager.registerListener(installStateListener) + updateListener?.onAppUpdateStarted(AppUpdateType.FLEXIBLE) + requestUpdate(AppUpdateType.FLEXIBLE, appUpdateInfo, activity) + } + + @Suppress("TooGenericExceptionCaught") + private fun requestUpdate(updateType: Int, appUpdateInfo: AppUpdateInfo, activity: Activity) { + saveLastUpdateRequestInfo(appUpdateInfo) + val requestCode = if (updateType == AppUpdateType.IMMEDIATE) { + APP_UPDATE_IMMEDIATE_REQUEST_CODE + } else { + APP_UPDATE_FLEXIBLE_REQUEST_CODE + } + try { + appUpdateManager.startUpdateFlowForResult( + appUpdateInfo, + activity, + AppUpdateOptions.newBuilder(updateType).build(), + requestCode + ) + inAppUpdateAnalyticsTracker.trackUpdateShown(updateType) + } catch (e: Exception) { + Log.e(TAG, "requestUpdate for type: $updateType, exception occurred") + Log.e(TAG, e.message.toString()) + appUpdateManager.unregisterListener(installStateListener) + } + } + + private val installStateListener = object : InstallStateUpdatedListener { + @SuppressLint("SwitchIntDef") + override fun onStateUpdate(state: InstallState) { + when (state.installStatus()) { + DOWNLOADED -> { + updateListener?.onAppUpdateDownloaded() + } + INSTALLED -> { + updateListener?.onAppUpdateInstalled() + appUpdateManager.unregisterListener(this) // 'this' refers to the listener object + } + CANCELED -> { + updateListener?.onAppUpdateCancelled() + appUpdateManager.unregisterListener(this) + } + FAILED -> { + updateListener?.onAppUpdateFailed() + appUpdateManager.unregisterListener(this) + } + PENDING -> { + updateListener?.onAppUpdatePending() + } + DOWNLOADING, INSTALLING, InstallStatus.UNKNOWN -> { + /* do nothing */ + } + } + } + } + + private fun isImmediateUpdateInProgress(appUpdateInfo: AppUpdateInfo) = + appUpdateInfo.updateAvailability() == DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS + && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) + && isImmediateUpdateNecessary() + + private fun saveLastUpdateRequestInfo(appUpdateInfo: AppUpdateInfo) { + val sharedPref = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + sharedPref.edit().apply { + putInt(KEY_LAST_APP_UPDATE_CHECK_VERSION, getAvailableUpdateAppVersion(appUpdateInfo)) + putLong(KEY_LAST_APP_UPDATE_CHECK_TIME, currentTimeProvider.invoke()) + apply() + } + } + + private fun resetLastUpdateRequestInfo() { + val sharedPref = applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + sharedPref.edit().apply { + putInt(KEY_LAST_APP_UPDATE_CHECK_VERSION, -1) + putLong(KEY_LAST_APP_UPDATE_CHECK_TIME, -1L) + apply() + } + } + + private fun getLastUpdateRequestedVersion() = + applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getInt(KEY_LAST_APP_UPDATE_CHECK_VERSION, -1) + + private fun getLastUpdateRequestedTime() = + applicationContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getLong(KEY_LAST_APP_UPDATE_CHECK_TIME, -1L) + + private fun shouldRequestFlexibleUpdate() = + currentTimeProvider.invoke() - getLastUpdateRequestedTime() >= getFlexibleUpdateIntervalInMillis() + + private fun shouldRequestImmediateUpdate() = + currentTimeProvider.invoke() - getLastUpdateRequestedTime() >= IMMEDIATE_UPDATE_INTERVAL_IN_MILLIS + + @Suppress("MagicNumber") + private fun getFlexibleUpdateIntervalInMillis(): Long = + 1000 * 60 * 60 * 24 * remoteConfigWrapper.getInAppUpdateFlexibleIntervalInDays().toLong() + + private fun getCurrentAppVersion() = buildConfigWrapper.getAppVersionCode() + + private fun getLastBlockingAppVersion(): Int = remoteConfigWrapper.getInAppUpdateBlockingVersion() + + private fun getAvailableUpdateAppVersion(appUpdateInfo: AppUpdateInfo) = appUpdateInfo.availableVersionCode() + + private fun isImmediateUpdateNecessary() = getCurrentAppVersion() < getLastBlockingAppVersion() + + companion object { + const val IMMEDIATE_UPDATE_INTERVAL_IN_MILLIS = 1000 * 60 * 5 // 5 minutes + const val KEY_LAST_APP_UPDATE_CHECK_TIME = "last_app_update_check_time" + + private const val TAG = "AppUpdateChecker" + private const val PREF_NAME = "in_app_update_prefs" + private const val KEY_LAST_APP_UPDATE_CHECK_VERSION = "last_app_update_check_version" + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerNoop.kt b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerNoop.kt new file mode 100644 index 000000000000..d732dc62d7e9 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/inappupdate/InAppUpdateManagerNoop.kt @@ -0,0 +1,21 @@ +package org.wordpress.android.inappupdate + +import android.app.Activity + +class InAppUpdateManagerNoop: IInAppUpdateManager { + override fun checkForAppUpdate(activity: Activity, listener: InAppUpdateListener) { + /* Empty implementation */ + } + + override fun completeAppUpdate() { + /* Empty implementation */ + } + + override fun cancelAppUpdate(updateType: Int) { + /* Empty implementation */ + } + + override fun onUserAcceptedAppUpdate(updateType: Int) { + /* Empty implementation */ + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java index 9814ec12f66a..ed712b9d32e3 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java @@ -8,11 +8,17 @@ import androidx.lifecycle.LiveData; import androidx.preference.PreferenceManager; +import com.google.android.play.core.appupdate.AppUpdateManager; +import com.google.android.play.core.appupdate.AppUpdateManagerFactory; import com.tenor.android.core.network.ApiClient; import com.tenor.android.core.network.ApiService; import com.tenor.android.core.network.IApiClient; import org.wordpress.android.BuildConfig; +import org.wordpress.android.inappupdate.IInAppUpdateManager; +import org.wordpress.android.inappupdate.InAppUpdateAnalyticsTracker; +import org.wordpress.android.inappupdate.InAppUpdateManagerImpl; +import org.wordpress.android.inappupdate.InAppUpdateManagerNoop; import org.wordpress.android.ui.ActivityNavigator; import org.wordpress.android.ui.jetpack.backup.download.BackupDownloadStep; import org.wordpress.android.ui.jetpack.backup.download.BackupDownloadStepsProvider; @@ -21,6 +27,9 @@ import org.wordpress.android.ui.mediapicker.loader.TenorGifClient; import org.wordpress.android.ui.sitecreation.SiteCreationStep; import org.wordpress.android.ui.sitecreation.SiteCreationStepsProvider; +import org.wordpress.android.util.BuildConfigWrapper; +import org.wordpress.android.util.config.InAppUpdatesFeatureConfig; +import org.wordpress.android.util.config.RemoteConfigWrapper; import org.wordpress.android.util.wizard.WizardManager; import org.wordpress.android.viewmodel.helpers.ConnectionStatus; import org.wordpress.android.viewmodel.helpers.ConnectionStatusLiveData; @@ -76,6 +85,33 @@ public static WizardManager provideRestoreWizardManager( return new WizardManager<>(stepsProvider.getSteps()); } + @Provides + public static AppUpdateManager provideAppUpdateManager(@ApplicationContext Context context) { + return AppUpdateManagerFactory.create(context); + } + + @Provides + public static IInAppUpdateManager provideInAppUpdateManager( + @ApplicationContext Context context, + AppUpdateManager appUpdateManager, + RemoteConfigWrapper remoteConfigWrapper, + BuildConfigWrapper buildConfigWrapper, + InAppUpdatesFeatureConfig inAppUpdatesFeatureConfig, + InAppUpdateAnalyticsTracker inAppUpdateAnalyticsTracker + ) { + // Check if in-app updates feature is enabled + return inAppUpdatesFeatureConfig.isEnabled() + ? new InAppUpdateManagerImpl( + context, + appUpdateManager, + remoteConfigWrapper, + buildConfigWrapper, + inAppUpdateAnalyticsTracker, + System::currentTimeMillis + ) + : new InAppUpdateManagerNoop(); + } + @Provides public static ActivityNavigator provideActivityNavigator(@ApplicationContext Context context) { return new ActivityNavigator(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index dffbacaeb591..d15191ccd33d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -10,6 +10,7 @@ import android.os.Handler; import android.os.Looper; import android.text.TextUtils; +import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.View; import android.view.ViewGroup; @@ -30,6 +31,7 @@ import com.google.android.gms.tasks.Task; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; +import com.google.android.play.core.install.model.AppUpdateType; import com.google.android.play.core.review.ReviewInfo; import com.google.android.play.core.review.ReviewManager; import com.google.android.play.core.review.ReviewManagerFactory; @@ -38,6 +40,8 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.wordpress.android.BuildConfig; +import org.wordpress.android.inappupdate.InAppUpdateListener; +import org.wordpress.android.inappupdate.IInAppUpdateManager; import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; @@ -174,6 +178,7 @@ import org.wordpress.android.viewmodel.mlp.ModalLayoutPickerViewModel; import org.wordpress.android.viewmodel.mlp.ModalLayoutPickerViewModel.CreatePageDashboardSource; import org.wordpress.android.widgets.AppRatingDialog; +import org.wordpress.android.widgets.WPSnackbar; import org.wordpress.android.workers.notification.createsite.CreateSiteNotificationScheduler; import org.wordpress.android.workers.weeklyroundup.WeeklyRoundupScheduler; @@ -296,6 +301,8 @@ public class WPMainActivity extends LocaleAwareActivity implements @Inject BuildConfigWrapper mBuildConfigWrapper; + @Inject IInAppUpdateManager mInAppUpdateManager; + @Inject GCMRegistrationScheduler mGCMRegistrationScheduler; @Inject ActivityNavigator mActivityNavigator; @@ -1196,9 +1203,30 @@ protected void onResume() { mSelectedSiteRepository.hasSelectedSite() && mBottomNav != null && mBottomNav.getCurrentSelectedPage() == PageType.MY_SITE ); + + checkForInAppUpdate(); + mIsChangingConfiguration = false; } + private void checkForInAppUpdate() { + mInAppUpdateManager.checkForAppUpdate(this, mInAppUpdateListener); + } + + @NonNull final InAppUpdateListener mInAppUpdateListener = new InAppUpdateListener() { + @Override public void onAppUpdateDownloaded() { + popupSnackbarForCompleteUpdate(); + } + }; + + private void popupSnackbarForCompleteUpdate() { + WPSnackbar.make(findViewById(R.id.coordinator), R.string.in_app_update_available, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.in_app_update_restart, v -> { + mInAppUpdateManager.completeAppUpdate(); + }) + .show(); + } + private void checkQuickStartNotificationStatus() { SiteModel selectedSite = getSelectedSite(); long selectedSiteLocalId = mSelectedSiteRepository.getSelectedSiteLocalId(); @@ -1350,6 +1378,7 @@ private void setSite(Intent data) { @Override @SuppressWarnings("deprecation") public void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.e("WPMainActivity", "onActivityResult: " + requestCode + " " + resultCode); super.onActivityResult(requestCode, resultCode, data); if (!mSelectedSiteRepository.hasSelectedSite()) { initSelectedSite(); @@ -1463,6 +1492,23 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { case RequestCodes.DOMAIN_REGISTRATION: passOnActivityResultToMySiteFragment(requestCode, resultCode, data); break; + case IInAppUpdateManager.APP_UPDATE_FLEXIBLE_REQUEST_CODE: + handleUpdateResult(resultCode, AppUpdateType.FLEXIBLE); + break; + case IInAppUpdateManager.APP_UPDATE_IMMEDIATE_REQUEST_CODE: + handleUpdateResult(resultCode, AppUpdateType.IMMEDIATE); + break; + } + } + + // Handles the result of the app update request + private void handleUpdateResult(int resultCode, int updateType) { + if (resultCode == RESULT_OK) { + // The user accepted the update + mInAppUpdateManager.onUserAcceptedAppUpdate(updateType); + } else if (resultCode == RESULT_CANCELED) { + // The user denied the update + mInAppUpdateManager.cancelAppUpdate(updateType); } } @@ -1884,6 +1930,7 @@ public void onSetPromptReminderClick(final int siteId) { onActivityResult(RequestCodes.SITE_PICKER, resultCode, data); } + // We dismiss the QuickStart SnackBar every time activity is paused because // SnackBar sometimes do not appear when another SnackBar is still visible, even in other activities (weird) @Override diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfig.kt new file mode 100644 index 000000000000..c6df149a0f0a --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfig.kt @@ -0,0 +1,17 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.annotation.RemoteFieldDefaultGenerater +import javax.inject.Inject + +const val IN_APP_UPDATE_BLOCKING_VERSION_DEFAULT = "0" + +@RemoteFieldDefaultGenerater( + remoteField = IN_APP_UPDATE_BLOCKING_VERSION_REMOTE_FIELD, + defaultValue = IN_APP_UPDATE_BLOCKING_VERSION_DEFAULT +) + +class InAppUpdateBlockingVersionConfig @Inject constructor(appConfig: AppConfig) : + RemoteConfigField( + appConfig, + IN_APP_UPDATE_BLOCKING_VERSION_REMOTE_FIELD + ) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/InAppUpdateFlexibleIntervalConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/InAppUpdateFlexibleIntervalConfig.kt new file mode 100644 index 000000000000..17541cc9dfea --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/InAppUpdateFlexibleIntervalConfig.kt @@ -0,0 +1,18 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.annotation.RemoteFieldDefaultGenerater +import javax.inject.Inject + +const val IN_APP_UPDATE_FLEXIBLE_INTERVAL_REMOTE_FIELD = "in_app_update_flexible_interval_in_days_android" +const val IN_APP_UPDATE_FLEXIBLE_INTERVAL_DEFAULT = "5" + +@RemoteFieldDefaultGenerater( + remoteField = IN_APP_UPDATE_FLEXIBLE_INTERVAL_REMOTE_FIELD, + defaultValue = IN_APP_UPDATE_FLEXIBLE_INTERVAL_DEFAULT +) + +class InAppUpdateFlexibleIntervalConfig @Inject constructor(appConfig: AppConfig) : + RemoteConfigField( + appConfig, + IN_APP_UPDATE_FLEXIBLE_INTERVAL_REMOTE_FIELD + ) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/InAppUpdatesFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/InAppUpdatesFeatureConfig.kt new file mode 100644 index 000000000000..3836cb91ff64 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/InAppUpdatesFeatureConfig.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.Feature +import javax.inject.Inject + +private const val IN_APP_UPDATES_FEATURE_REMOTE_FIELD = "in_app_updates" + +@Feature(IN_APP_UPDATES_FEATURE_REMOTE_FIELD, false) +class InAppUpdatesFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.ENABLE_IN_APP_UPDATES, + IN_APP_UPDATES_FEATURE_REMOTE_FIELD +) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/RemoteConfigWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/config/RemoteConfigWrapper.kt index 3113d9b84bfa..eae8223994d6 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/config/RemoteConfigWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/config/RemoteConfigWrapper.kt @@ -7,9 +7,13 @@ import javax.inject.Singleton class RemoteConfigWrapper @Inject constructor( private val openWebLinksWithJetpackFlowFrequencyConfig: OpenWebLinksWithJetpackFlowFrequencyConfig, private val codeableGetFreeEstimateUrlConfig: CodeableGetFreeEstimateUrlConfig, - private val performanceMonitoringSampleRateConfig: PerformanceMonitoringSampleRateConfig + private val performanceMonitoringSampleRateConfig: PerformanceMonitoringSampleRateConfig, + private val inAppUpdateBlockingVersionConfig: InAppUpdateBlockingVersionConfig, + private val inAppUpdateFlexibleIntervalConfig: InAppUpdateFlexibleIntervalConfig, ) { fun getOpenWebLinksWithJetpackFlowFrequency() = openWebLinksWithJetpackFlowFrequencyConfig.getValue() fun getPerformanceMonitoringSampleRate() = performanceMonitoringSampleRateConfig.getValue() fun getCodeableGetFreeEstimateUrl() = codeableGetFreeEstimateUrlConfig.getValue() + fun getInAppUpdateBlockingVersion() = inAppUpdateBlockingVersionConfig.getValue() + fun getInAppUpdateFlexibleIntervalInDays() = inAppUpdateFlexibleIntervalConfig.getValue() } diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 60f8764c813a..b29f740f7c9d 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -4817,6 +4817,9 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> There was some trouble with the Security key login Please provide your security key to continue. + Update downloaded. Restart to apply. + Restart + Alternatively, you can flatten the content by ungrouping the block. For this reason, we recommend editing the block using the web editor. For this reason, we recommend editing the block using your web browser. diff --git a/WordPress/src/test/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTrackerTest.kt new file mode 100644 index 000000000000..12932813e511 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/inappupdate/InAppUpdateAnalyticsTrackerTest.kt @@ -0,0 +1,133 @@ +package org.wordpress.android.inappupdate + +import com.google.android.play.core.install.model.AppUpdateType +import org.assertj.core.api.Assertions +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.mock +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.inappupdate.InAppUpdateAnalyticsTracker.Companion.PROPERTY_UPDATE_TYPE +import org.wordpress.android.inappupdate.InAppUpdateAnalyticsTracker.Companion.UPDATE_TYPE_BLOCKING +import org.wordpress.android.inappupdate.InAppUpdateAnalyticsTracker.Companion.UPDATE_TYPE_FLEXIBLE + +@RunWith(MockitoJUnitRunner::class) +class InAppUpdateAnalyticsTrackerTest { + private val analyticsTracker: AnalyticsTrackerWrapper = mock() + lateinit var tracker: InAppUpdateAnalyticsTracker + + private val flexibleProps = mapOf( + PROPERTY_UPDATE_TYPE to UPDATE_TYPE_FLEXIBLE + ) + private val blockingProps = mapOf( + PROPERTY_UPDATE_TYPE to UPDATE_TYPE_BLOCKING + ) + private val emptyProps = emptyMap() + + @Before + fun setUp() { + tracker = InAppUpdateAnalyticsTracker(analyticsTracker) + } + + @Test + fun `trackUpdateShown tracks flexible update shown`() { + tracker.trackUpdateShown(AppUpdateType.FLEXIBLE) + verifyCorrectEventTracking( + expectedEvent = AnalyticsTracker.Stat.IN_APP_UPDATE_SHOWN, + expectedProps = flexibleProps + ) + } + + @Test + fun `trackUpdateShown tracks immediate update shown`() { + tracker.trackUpdateShown(AppUpdateType.IMMEDIATE) + verifyCorrectEventTracking( + expectedEvent = AnalyticsTracker.Stat.IN_APP_UPDATE_SHOWN, + expectedProps = blockingProps + ) + } + + @Test + fun `trackUpdateShown tracks invalid update shown`() { + tracker.trackUpdateShown(-1) + verifyCorrectEventTracking( + expectedEvent = AnalyticsTracker.Stat.IN_APP_UPDATE_SHOWN, + expectedProps = emptyProps + ) + } + + @Test + fun `trackUpdateAccepted tracks flexible update accepted`() { + tracker.trackUpdateAccepted(AppUpdateType.FLEXIBLE) + verifyCorrectEventTracking( + expectedEvent = AnalyticsTracker.Stat.IN_APP_UPDATE_ACCEPTED, + expectedProps = flexibleProps + ) + } + + @Test + fun `trackUpdateAccepted tracks immediate update accepted`() { + tracker.trackUpdateAccepted(AppUpdateType.IMMEDIATE) + verifyCorrectEventTracking( + expectedEvent = AnalyticsTracker.Stat.IN_APP_UPDATE_ACCEPTED, + expectedProps = blockingProps + ) + } + + @Test + fun `trackUpdateAccepted tracks invalid update accepted`() { + tracker.trackUpdateAccepted(-1) + verifyCorrectEventTracking( + expectedEvent = AnalyticsTracker.Stat.IN_APP_UPDATE_ACCEPTED, + expectedProps = emptyProps + ) + } + + @Test + fun `trackUpdateDismissed tracks flexible update dismissed`() { + tracker.trackUpdateDismissed(AppUpdateType.FLEXIBLE) + verifyCorrectEventTracking( + expectedEvent = AnalyticsTracker.Stat.IN_APP_UPDATE_DISMISSED, + expectedProps = flexibleProps + ) + } + + @Test + fun `trackUpdateDismissed tracks immediate update dismissed`() { + tracker.trackUpdateDismissed(AppUpdateType.IMMEDIATE) + verifyCorrectEventTracking( + expectedEvent = AnalyticsTracker.Stat.IN_APP_UPDATE_DISMISSED, + expectedProps = blockingProps + ) + } + + @Test + fun `trackUpdateDismissed tracks invalid update dismissed`() { + tracker.trackUpdateDismissed(-1) + verifyCorrectEventTracking( + expectedEvent = AnalyticsTracker.Stat.IN_APP_UPDATE_DISMISSED, + expectedProps = emptyProps + ) + } + + private fun mapCaptor() = argumentCaptor>() + private fun verifyCorrectEventTracking( + expectedEvent: AnalyticsTracker.Stat, + expectedProps: Map, + expectedTimes: Int = 1 + ) { + mapCaptor().apply { + verify(analyticsTracker, times(expectedTimes)).track( + eq(expectedEvent), + capture() + ) + Assertions.assertThat(firstValue).isEqualTo(expectedProps) + } + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/inappupdate/InAppUpdateManagerImplTest.kt b/WordPress/src/test/java/org/wordpress/android/inappupdate/InAppUpdateManagerImplTest.kt new file mode 100644 index 000000000000..c54ba84bfd1e --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/inappupdate/InAppUpdateManagerImplTest.kt @@ -0,0 +1,235 @@ +package org.wordpress.android.inappupdate + +import android.app.Activity +import android.content.Context +import android.content.SharedPreferences +import com.google.android.gms.tasks.OnFailureListener +import com.google.android.gms.tasks.OnSuccessListener +import com.google.android.gms.tasks.Task +import com.google.android.play.core.appupdate.AppUpdateInfo +import com.google.android.play.core.appupdate.AppUpdateManager +import com.google.android.play.core.appupdate.AppUpdateOptions +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 org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any +import org.mockito.kotlin.verify +import org.wordpress.android.inappupdate.IInAppUpdateManager.Companion.APP_UPDATE_FLEXIBLE_REQUEST_CODE +import org.wordpress.android.inappupdate.IInAppUpdateManager.Companion.APP_UPDATE_IMMEDIATE_REQUEST_CODE +import org.wordpress.android.inappupdate.InAppUpdateManagerImpl.Companion.IMMEDIATE_UPDATE_INTERVAL_IN_MILLIS +import org.wordpress.android.inappupdate.InAppUpdateManagerImpl.Companion.KEY_LAST_APP_UPDATE_CHECK_TIME +import org.wordpress.android.util.BuildConfigWrapper +import org.wordpress.android.util.config.RemoteConfigWrapper + +@RunWith(MockitoJUnitRunner::class) +class InAppUpdateManagerImplTest { + @Mock + lateinit var applicationContext: Context + + @Mock + lateinit var appUpdateManager: AppUpdateManager + + @Mock + lateinit var remoteConfigWrapper: RemoteConfigWrapper + + @Mock + lateinit var buildConfigWrapper: BuildConfigWrapper + + @Mock + lateinit var inAppUpdateAnalyticsTracker: InAppUpdateAnalyticsTracker + + @Mock + lateinit var updateListener: InAppUpdateListener + + @Mock + lateinit var activity: Activity + + @Mock + lateinit var appUpdateInfo: AppUpdateInfo + + @Mock + lateinit var sharedPreferences: SharedPreferences + + @Mock + lateinit var sharedPreferencesEditor: SharedPreferences.Editor + + lateinit var currentTimeProvider: () -> Long + + lateinit var inAppUpdateManager: InAppUpdateManagerImpl + + @Before + fun setUp() { + currentTimeProvider = {1715866314746L} // Thu May 16 2024 13:31:54 UTC + + // Mock SharedPreferences behavior + `when`(applicationContext.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPreferences) + `when`(sharedPreferences.getInt(anyString(), anyInt())).thenReturn(-1) + `when`(sharedPreferences.edit()).thenReturn(sharedPreferencesEditor) + `when`(sharedPreferencesEditor.putInt(anyString(), anyInt())).thenReturn(sharedPreferencesEditor) + `when`(sharedPreferencesEditor.putLong(anyString(), anyLong())).thenReturn(sharedPreferencesEditor) + + inAppUpdateManager = InAppUpdateManagerImpl( + applicationContext, + appUpdateManager, + remoteConfigWrapper, + buildConfigWrapper, + inAppUpdateAnalyticsTracker, + currentTimeProvider + ) + } + + @Test + fun `checkForAppUpdate when update is not available does not trigger update`() { + // Arrange + val task = mockAppUpdateInfoTask(appUpdateInfo) + `when`(appUpdateManager.appUpdateInfo).thenReturn(task) + `when`(appUpdateInfo.updateAvailability()).thenReturn(UpdateAvailability.UPDATE_NOT_AVAILABLE) + + // Act + inAppUpdateManager.checkForAppUpdate(activity, updateListener) + + // Assert + verify(appUpdateManager.appUpdateInfo).addOnSuccessListener(any()) + verify(appUpdateManager, times(0)).startUpdateFlowForResult( + any(), + any(), + any(), + anyInt() + ) + } + + @Test + fun `checkForAppUpdate when update is downloaded calls update listener`() { + // Arrange + val task = mockAppUpdateInfoTask(appUpdateInfo) + `when`(appUpdateManager.appUpdateInfo).thenReturn(task) + `when`(appUpdateInfo.updateAvailability()).thenReturn(UpdateAvailability.UPDATE_AVAILABLE) + `when`(appUpdateInfo.installStatus()).thenReturn(InstallStatus.DOWNLOADED) + + // Act + inAppUpdateManager.checkForAppUpdate(activity, updateListener) + + // Assert + verify(updateListener).onAppUpdateDownloaded() + } + + @Test + fun `checkForAppUpdate requests immediate update when necessary`() { + // Arrange + val task = mockAppUpdateInfoTask(appUpdateInfo) + `when`(appUpdateManager.appUpdateInfo).thenReturn(task) + `when`(appUpdateInfo.updateAvailability()).thenReturn(UpdateAvailability.UPDATE_AVAILABLE) + `when`(appUpdateInfo.installStatus()).thenReturn(InstallStatus.UNKNOWN) + `when`(buildConfigWrapper.getAppVersionCode()).thenReturn(100) // current version + `when`(remoteConfigWrapper.getInAppUpdateBlockingVersion()).thenReturn(200) // blocking version + val lastCheckTimestamp = currentTimeProvider.invoke() - IMMEDIATE_UPDATE_INTERVAL_IN_MILLIS + `when`(sharedPreferences.getLong( eq(KEY_LAST_APP_UPDATE_CHECK_TIME), anyLong())).thenReturn(lastCheckTimestamp) + + // Act + inAppUpdateManager.checkForAppUpdate(activity, updateListener) + + // Assert + verify(appUpdateManager).startUpdateFlowForResult( + any(), + any(), + any(), + eq(APP_UPDATE_IMMEDIATE_REQUEST_CODE) + ) + } + + @Test + fun `checkForAppUpdate requests flexible update when necessary`() { + // Arrange + val task = mockAppUpdateInfoTask(appUpdateInfo) + `when`(appUpdateManager.appUpdateInfo).thenReturn(task) + `when`(appUpdateInfo.updateAvailability()).thenReturn(UpdateAvailability.UPDATE_AVAILABLE) + `when`(appUpdateInfo.installStatus()).thenReturn(InstallStatus.UNKNOWN) + `when`(buildConfigWrapper.getAppVersionCode()).thenReturn(100) // current version + `when`(remoteConfigWrapper.getInAppUpdateBlockingVersion()).thenReturn(50) // blocking version + `when`(remoteConfigWrapper.getInAppUpdateFlexibleIntervalInDays()).thenReturn(1) + val lastCheckTimestamp = currentTimeProvider.invoke() - 1000*60*60*24 + `when`(sharedPreferences.getLong( eq(KEY_LAST_APP_UPDATE_CHECK_TIME), anyLong())).thenReturn(lastCheckTimestamp) + + // Act + inAppUpdateManager.checkForAppUpdate(activity, updateListener) + + // Assert + verify(appUpdateManager).startUpdateFlowForResult( + any(), + any(), + any(), + eq(APP_UPDATE_FLEXIBLE_REQUEST_CODE) + ) + } + + @Test + fun `checkForAppUpdate handles developer triggered update in progress`() { + // Arrange + val task = mockAppUpdateInfoTask(appUpdateInfo) + `when`(appUpdateManager.appUpdateInfo).thenReturn(task) + `when`(appUpdateInfo.updateAvailability()).thenReturn(UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) + `when`(appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)).thenReturn(true) + `when`(buildConfigWrapper.getAppVersionCode()).thenReturn(100) + `when`(remoteConfigWrapper.getInAppUpdateBlockingVersion()).thenReturn(200) + + // Act + inAppUpdateManager.checkForAppUpdate(activity, updateListener) + + // Assert + verify(appUpdateManager).startUpdateFlowForResult( + eq(appUpdateInfo), + eq(activity), + any(), + eq(APP_UPDATE_IMMEDIATE_REQUEST_CODE) + ) + } + + @Test + fun `checkForAppUpdate handles failure correctly`() { + // Arrange + val task = mockAppUpdateInfoTaskWithFailure() + `when`(appUpdateManager.appUpdateInfo).thenReturn(task) + + // Act + inAppUpdateManager.checkForAppUpdate(activity, updateListener) + + // Assert + verify(appUpdateManager.appUpdateInfo).addOnFailureListener(any()) + } + + // Helper method to mock Task with success + @Suppress("UNCHECKED_CAST") + private fun mockAppUpdateInfoTask(appUpdateInfo: AppUpdateInfo): Task { + val task = mock(Task::class.java) as Task + `when`(task.addOnSuccessListener(any())).thenAnswer { invocation -> + (invocation.arguments[0] as OnSuccessListener).onSuccess(appUpdateInfo) + task + } + `when`(task.addOnFailureListener(any())).thenReturn(task) + return task + } + + // Helper method to mock Task with failure + @Suppress("UNCHECKED_CAST") + private fun mockAppUpdateInfoTaskWithFailure(): Task { + val task = mock(Task::class.java) as Task + `when`(task.addOnFailureListener(any())).thenAnswer { invocation -> + (invocation.arguments[0] as OnFailureListener).onFailure(Exception("Update check failed")) + task + } + `when`(task.addOnSuccessListener(any())).thenReturn(task) + return task + } +} diff --git a/WordPress/src/wordpress/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfigConstants.kt b/WordPress/src/wordpress/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfigConstants.kt new file mode 100644 index 000000000000..168bda3e189a --- /dev/null +++ b/WordPress/src/wordpress/java/org/wordpress/android/util/config/InAppUpdateBlockingVersionConfigConstants.kt @@ -0,0 +1,5 @@ +package org.wordpress.android.util.config + +const val IN_APP_UPDATE_BLOCKING_VERSION_REMOTE_FIELD = "wp_in_app_update_blocking_version_android" + + diff --git a/build.gradle b/build.gradle index df7da43a7353..c07a8e74de6a 100644 --- a/build.gradle +++ b/build.gradle @@ -90,6 +90,7 @@ ext { squareupRetrofitVersion = '2.9.0' uCropVersion = '2.2.9' zendeskVersion = '5.1.2' + googlePlayInAppUpdateVersion = '2.1.0' // react native facebookReactVersion = '0.73.3' diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index 1cd7bc461d0c..c154d9cdd62d 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -1122,7 +1122,12 @@ public enum Stat { RESOLVE_AUTOSAVE_CONFLICT_CONFIRM_TAPPED, RESOLVE_AUTOSAVE_CONFLICT_CANCEL_TAPPED, RESOLVE_AUTOSAVE_CONFLICT_CLOSE_TAPPED, - RESOLVE_AUTOSAVE_CONFLICT_DISMISSED; + RESOLVE_AUTOSAVE_CONFLICT_DISMISSED, + IN_APP_UPDATE_SHOWN, + IN_APP_UPDATE_DISMISSED, + IN_APP_UPDATE_ACCEPTED, + IN_APP_UPDATE_COMPLETED_WITH_APP_RESTART; + /* * Please set the event name in the enum only if the new Stat's name in lower case does not match it. * In that case you also need to add the event in the `AnalyticsTrackerNosaraTest.specialNames` map. diff --git a/version.properties b/version.properties index e5fe7660ff37..0e9ac9f04ba4 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ versionName=24.9-rc-1 -versionCode=1431 \ No newline at end of file +versionCode=1431