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

Feature: IN-APP UPDATES #20822

Merged
merged 54 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
58881a2
Adds: dependency for In app updates
AjeshRPai Mar 23, 2023
23adf5c
Adds: App update checker class to the project
AjeshRPai Mar 23, 2023
2ace10e
Adds: logic to start google play update
AjeshRPai Mar 24, 2023
e4f8a6d
Removes: in app update checker from app initializer
AjeshRPai Mar 28, 2023
a324862
Moves: the dependency implementation and dependency version
AjeshRPai Mar 28, 2023
c237e21
Fixes: Merge conflict in MySiteViewModelTest
AjeshRPai Mar 28, 2023
6c0235f
Adds: Helper functions to check whether update is in progress
AjeshRPai Mar 28, 2023
5263ce6
Adds: the function to get the app update information
AjeshRPai Mar 28, 2023
7cf441f
Fixes: google play update version
AjeshRPai Apr 3, 2023
7ae9956
Fixes: lint issue for now
AjeshRPai Apr 3, 2023
dd46058
Adds: logs for checking the update status
AjeshRPai Apr 3, 2023
79dc058
Adds: Dagger logic for InAppUpdate manager
AjeshRPai Apr 3, 2023
61f287b
Adds: the logic for checking the updates on OnResume
AjeshRPai Apr 3, 2023
92b2f08
Merge branch 'trunk' into issue/in-app-updates
AjeshRPai Nov 16, 2023
a74c72d
↑ Updates: the app version for testing purposes
AjeshRPai Nov 16, 2023
dcb8805
↑ Updates: the usage to Wp Snackbar
AjeshRPai Nov 16, 2023
5089d99
+ Adds: more logging to the update manager
AjeshRPai Nov 17, 2023
ad2a75d
↑ Updates: the app update manager to check for update status
AjeshRPai Nov 17, 2023
f3c9bbe
↑ Updates: the install state listener to use various download status
AjeshRPai Nov 17, 2023
44b5d4c
Merge branch 'trunk' into issue/in-app-updates
ravishanker Dec 11, 2023
13bd1d7
Merge branch 'trunk' into issue/in-app-updates
AjeshRPai Dec 15, 2023
e057a53
Merge branch 'trunk' into issue/in-app-updates
ravishanker Dec 16, 2023
e77a27a
Merge branch 'trunk' into issue/in-app-updates
ravishanker Dec 19, 2023
53e3af6
Merge branch 'trunk' into issue/in-app-updates
AjeshRPai Dec 27, 2023
a9793bf
Merge branch 'trunk' into issue/in-app-updates
AjeshRPai May 6, 2024
bcf8906
* Fixes: formatting
AjeshRPai May 6, 2024
0dbd588
* Fixes: redundant imports
AjeshRPai May 6, 2024
8d030fb
* Fixes: Checkstyle error
AjeshRPai May 6, 2024
9eaca95
* Fixes: detekt issues
AjeshRPai May 6, 2024
9180ab8
Updates: in app updates lib version
pantstamp May 7, 2024
0f28380
Adds: InAppUpdateBlockingVersionConfig
pantstamp May 7, 2024
fb0cc20
Adds: functions in InAppUpdateManager for immediate updates
pantstamp May 7, 2024
19bda63
Refactors: InAppUpdateManager
pantstamp May 8, 2024
a819803
Adds: separate inappupdate remote configs for wp and jp apps
pantstamp May 9, 2024
a52d665
Fixes: InAppUpdateManager minor fixes
pantstamp May 9, 2024
56144da
wip
pantstamp May 9, 2024
6784eff
Adds: debug logs for InAppUpdateManager
pantstamp May 14, 2024
16f644d
Updates: InAppUpdateManager, reset saved values if new update is avai…
pantstamp May 14, 2024
ce83a3b
Adds: InAppUpdateBlockingVersionConfig for both jp and wp apps
pantstamp May 14, 2024
b0d4ebc
Modifies: in app update snackbar
pantstamp May 14, 2024
1995dd3
Adds: InAppUpdatesFeatureConfig
pantstamp May 15, 2024
223dadf
Adds: analytics for in-app updates
pantstamp May 15, 2024
49b4229
Fixes: checkstyle and detekt issues
pantstamp May 15, 2024
e21f6d0
Refactors: InAppUpdateMaangerImpl
pantstamp May 15, 2024
36e7569
Adds: remote config for flexible updates interval
pantstamp May 15, 2024
a5a84c8
Fixes: detekt issue
pantstamp May 15, 2024
82f908c
Fixes: in-app updated FF
pantstamp May 15, 2024
74a185c
Merge branch 'trunk' into pantelis/in-app-updates
pantstamp May 15, 2024
267ea8d
Adds: property for trackUpdateDismissed
pantstamp May 16, 2024
a488fe6
Adds: check interval for immediate updates
pantstamp May 16, 2024
d7963e6
Adds: unit tests for in-app updates
pantstamp May 16, 2024
ef66d6f
Removes: logs from in-app update feature
pantstamp May 17, 2024
ec5dbb6
Adds: tracking for app restart to compete app update
pantstamp May 20, 2024
c138065
Refactors: InAppUpdateListener
pantstamp May 20, 2024
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 WordPress/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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"


Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.wordpress.android.inappupdate

interface IInAppUpdateListener {
fun onAppUpdateStarted(type: Int)
fun onAppUpdateDownloaded()
fun onAppUpdateInstalled()
fun onAppUpdateFailed()
fun onAppUpdateCancelled()
fun onAppUpdatePending()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.wordpress.android.inappupdate

import android.app.Activity

interface IInAppUpdateManager {
fun checkForAppUpdate(activity: Activity, listener: IInAppUpdateListener)
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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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))
}

private fun createPropertyMap(updateType: Int): Map<String, String> {
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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
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: IInAppUpdateListener? = null

override fun checkForAppUpdate(activity: Activity, listener: IInAppUpdateListener) {
updateListener = listener
appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
handleUpdateInfoSuccess(appUpdateInfo, activity)
}.addOnFailureListener { exception ->
Log.e(TAG, "Failed to check for update: ${exception.message}")
}
}

override fun completeAppUpdate() {
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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.wordpress.android.inappupdate

import android.app.Activity

class InAppUpdateManagerNoop: IInAppUpdateManager {
AjeshRPai marked this conversation as resolved.
Show resolved Hide resolved
override fun checkForAppUpdate(activity: Activity, listener: IInAppUpdateListener) {
/* Empty implementation */
}

override fun completeAppUpdate() {
/* Empty implementation */
}

override fun cancelAppUpdate(updateType: Int) {
/* Empty implementation */
}

override fun onUserAcceptedAppUpdate(updateType: Int) {
/* Empty implementation */
}
}
Loading
Loading