Skip to content

Commit

Permalink
Merge pull request #1590 from Infomaniak/Add-worker-to-update-app
Browse files Browse the repository at this point in the history
Add worker to update app in background
  • Loading branch information
sirambd authored Dec 6, 2023
2 parents 2a8b86c + 207058a commit edcfd4a
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 19 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ dependencies {
implementation libs.flexbox
implementation libs.lifecycle.process
implementation libs.webkit
implementation libs.work.concurrent.futures
implementation libs.work.runtime.ktx

implementation libs.hilt.android
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/java/com/infomaniak/mail/MainApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import com.infomaniak.mail.di.IoDispatcher
import com.infomaniak.mail.di.MainDispatcher
import com.infomaniak.mail.ui.LaunchActivity
import com.infomaniak.mail.utils.*
import com.infomaniak.mail.workers.AppUpdateWorker
import com.infomaniak.mail.workers.SyncMailboxesWorker
import dagger.hilt.android.HiltAndroidApp
import io.sentry.SentryEvent
Expand Down Expand Up @@ -100,6 +101,9 @@ open class MainApplication : Application(), ImageLoaderFactory, DefaultLifecycle
@Inject
lateinit var syncMailboxesWorkerScheduler: SyncMailboxesWorker.Scheduler

@Inject
lateinit var appUpdateWorkerScheduler: AppUpdateWorker.Scheduler

@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
Expand Down Expand Up @@ -132,12 +136,16 @@ open class MainApplication : Application(), ImageLoaderFactory, DefaultLifecycle
override fun onStart(owner: LifecycleOwner) {
isAppInBackground = false
syncMailboxesWorkerScheduler.cancelWork()
appUpdateWorkerScheduler.cancelWork()
}

override fun onStop(owner: LifecycleOwner) {
lastAppClosingTime = Date().time
isAppInBackground = true
owner.lifecycleScope.launch { syncMailboxesWorkerScheduler.scheduleWorkIfNeeded() }
owner.lifecycleScope.launch {
syncMailboxesWorkerScheduler.scheduleWorkIfNeeded()
appUpdateWorkerScheduler.scheduleWorkIfNeeded()
}
}

private fun configureDebugMode() {
Expand Down
13 changes: 12 additions & 1 deletion app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,19 @@ class LocalSettings private constructor(context: Context) {
set(value) = sharedPreferences.transaction { putBoolean(IS_APP_LOCKED_KEY, value) }
//endregion

//region Update later
//region Update
var isUserWantingUpdates: Boolean
get() = sharedPreferences.getBoolean(IS_USER_WANTING_UPDATES_KEY, DEFAULT_IS_USER_WANTING_UPDATES)
set(value) = sharedPreferences.transaction { putBoolean(IS_USER_WANTING_UPDATES_KEY, value) }

var hasAppUpdateDownloaded: Boolean
get() = sharedPreferences.getBoolean(HAS_APP_UPDATE_DOWNLOADED_KEY, DEFAULT_HAS_APP_UPDATE_DOWNLOADED)
set(value) = sharedPreferences.transaction { putBoolean(HAS_APP_UPDATE_DOWNLOADED_KEY, value) }

fun resetUpdateSettings() {
isUserWantingUpdates = false // This avoid the user being instantly reprompted to download update
hasAppUpdateDownloaded = false
}
//endregion

//region Ai engine
Expand Down Expand Up @@ -345,6 +354,7 @@ class LocalSettings private constructor(context: Context) {
private const val DEFAULT_HAS_ALREADY_ENABLED_NOTIFICATIONS = false
private const val DEFAULT_IS_APP_LOCKED = false
private const val DEFAULT_IS_USER_WANTING_UPDATES = true
private const val DEFAULT_HAS_APP_UPDATE_DOWNLOADED = false
private val DEFAULT_AI_ENGINE = AiEngine.FALCON
private val DEFAULT_THREAD_DENSITY = ThreadDensity.LARGE
private val DEFAULT_THEME = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) Theme.LIGHT else Theme.SYSTEM
Expand Down Expand Up @@ -374,6 +384,7 @@ class LocalSettings private constructor(context: Context) {
private const val HAS_ALREADY_ENABLED_NOTIFICATIONS_KEY = "hasAlreadyEnabledNotificationsKey"
private const val IS_APP_LOCKED_KEY = "isAppLockedKey"
private const val IS_USER_WANTING_UPDATES_KEY = "isUserWantingUpdatesKey"
private const val HAS_APP_UPDATE_DOWNLOADED_KEY = "hasAppUpdateDownloaded"
private const val AI_ENGINE_KEY = "aiEngineKey"
private const val THREAD_DENSITY_KEY = "threadDensityKey"
private const val THEME_KEY = "themeKey"
Expand Down
9 changes: 6 additions & 3 deletions app/src/main/java/com/infomaniak/mail/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import com.infomaniak.lib.core.utils.SentryLog
import com.infomaniak.lib.core.utils.Utils
import com.infomaniak.lib.core.utils.Utils.toEnumOrThrow
import com.infomaniak.lib.core.utils.UtilsUi.openUrl
import com.infomaniak.lib.stores.StoreUtils.checkStalledUpdate
import com.infomaniak.lib.stores.StoreUtils.checkUpdateIsAvailable
import com.infomaniak.lib.stores.StoreUtils.initAppUpdateManager
import com.infomaniak.lib.stores.StoreUtils.launchInAppReview
Expand Down Expand Up @@ -327,7 +326,7 @@ class MainActivity : BaseActivity() {
super.onResume()
playServicesUtils.checkPlayServices(this)

checkStalledUpdate()
mainViewModel.checkAppUpdateStatus()

if (binding.drawerLayout.isOpen) colorSystemBarsWithMenuDrawer()
}
Expand Down Expand Up @@ -462,7 +461,11 @@ class MainActivity : BaseActivity() {
}

private fun initAppUpdateManager() {
initAppUpdateManager(this) { mainViewModel.canInstallUpdate.value = true }
initAppUpdateManager(
context = this,
onUpdateDownloaded = { mainViewModel.toggleAppUpdateStatus(isUpdateDownloaded = true) },
onUpdateInstalled = { mainViewModel.toggleAppUpdateStatus(isUpdateDownloaded = false) },
)
}

private fun showUpdateAvailable() = with(localSettings) {
Expand Down
17 changes: 15 additions & 2 deletions app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ import com.infomaniak.lib.core.models.ApiResponse
import com.infomaniak.lib.core.utils.ApiErrorCode.Companion.translateError
import com.infomaniak.lib.core.utils.SentryLog
import com.infomaniak.lib.core.utils.SingleLiveEvent
import com.infomaniak.lib.stores.StoreUtils
import com.infomaniak.mail.MatomoMail.trackMultiSelectionEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.api.ApiRepository
import com.infomaniak.mail.data.cache.RealmDatabase
import com.infomaniak.mail.data.cache.mailboxContent.*
import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.*
import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshCallbacks
import com.infomaniak.mail.data.cache.mailboxContent.RefreshController.RefreshMode
import com.infomaniak.mail.data.cache.mailboxInfo.MailboxController
import com.infomaniak.mail.data.cache.mailboxInfo.PermissionsController
import com.infomaniak.mail.data.cache.mailboxInfo.QuotasController
Expand All @@ -40,7 +43,6 @@ import com.infomaniak.mail.data.models.MoveResult
import com.infomaniak.mail.data.models.correspondent.Recipient
import com.infomaniak.mail.data.models.mailbox.Mailbox
import com.infomaniak.mail.data.models.message.Message
import com.infomaniak.mail.data.models.message.Message.*
import com.infomaniak.mail.data.models.thread.Thread
import com.infomaniak.mail.data.models.thread.Thread.ThreadFilter
import com.infomaniak.mail.di.IoDispatcher
Expand Down Expand Up @@ -72,6 +74,7 @@ class MainViewModel @Inject constructor(
private val addressBookController: AddressBookController,
private val draftController: DraftController,
private val folderController: FolderController,
private val localSettings: LocalSettings,
private val mailboxContentRealm: RealmDatabase.MailboxContent,
private val mailboxController: MailboxController,
private val mergedContactController: MergedContactController,
Expand Down Expand Up @@ -1014,6 +1017,16 @@ class MainViewModel @Inject constructor(
}
}

fun checkAppUpdateStatus() {
canInstallUpdate.value = localSettings.hasAppUpdateDownloaded
StoreUtils.checkStalledUpdate()
}

fun toggleAppUpdateStatus(isUpdateDownloaded: Boolean) {
canInstallUpdate.value = isUpdateDownloaded
localSettings.hasAppUpdateDownloaded = isUpdateDownloaded
}

companion object {
private val TAG: String = MainViewModel::class.java.simpleName
private val DEFAULT_SELECTED_FOLDER = FolderRole.INBOX
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ class ThreadListFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {
}

private fun refreshThreadsIfNotificationsAreDisabled() = with(mainViewModel) {
val areGoogleServicesDisabled = playServicesUtils.areGooglePlayServicesNotAvailable()
val areGoogleServicesDisabled = !playServicesUtils.areGooglePlayServicesAvailable()
val areAppNotifsDisabled = !notificationManagerCompat.areNotificationsEnabled()
val areMailboxNotifsDisabled = currentMailbox.value?.notificationsIsDisabled(notificationManagerCompat) == true
val shouldRefreshThreads = areGoogleServicesDisabled || areAppNotifsDisabled || areMailboxNotifsDisabled
Expand Down Expand Up @@ -578,12 +578,11 @@ class ThreadListFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {
trackEvent("inAppUpdate", "installUpdate")
mainViewModel.canInstallUpdate.value = false

StoreUtils.installDownloadedUpdate {
StoreUtils.installDownloadedUpdate(onFailure = {
Sentry.captureException(it)
// This avoid the user being instantly reprompted to download update
localSettings.isUserWantingUpdates = false
localSettings.resetUpdateSettings()
mainViewModel.snackBarManager.setValue(getString(RCore.string.errorUpdateInstall))
}
})
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface PlayServicesUtils {

fun checkPlayServices(fragmentActivity: FragmentActivity) = Unit

fun areGooglePlayServicesNotAvailable(): Boolean = true
fun areGooglePlayServicesAvailable(): Boolean = false

fun deleteFirebaseToken() = Unit
}
99 changes: 99 additions & 0 deletions app/src/main/java/com/infomaniak/mail/workers/AppUpdateWorker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Infomaniak Mail - Android
* Copyright (C) 2023 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.mail.workers

import android.content.Context
import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.hilt.work.HiltWorker
import androidx.work.*
import com.google.common.util.concurrent.ListenableFuture
import com.infomaniak.lib.core.utils.SentryLog
import com.infomaniak.lib.stores.StoreUtils
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.di.IoDispatcher
import com.infomaniak.mail.utils.PlayServicesUtils
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.sentry.Sentry
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton

@HiltWorker
class AppUpdateWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val localSettings: LocalSettings,
) : ListenableWorker(appContext, params) {

override fun startWork(): ListenableFuture<Result> {
SentryLog.i(TAG, "Work started")

return CallbackToFutureAdapter.getFuture { completer ->

StoreUtils.installDownloadedUpdate(
onSuccess = { completer.setResult(Result.success()) },
onFailure = { exception ->
localSettings.apply {
isUserWantingUpdates = false // This avoid the user being instantly reprompted to download update
localSettings.hasAppUpdateDownloaded = false
}
Sentry.captureException(exception)
localSettings.resetUpdateSettings()
completer.setResult(Result.failure())
},
)
}
}

private fun CallbackToFutureAdapter.Completer<Result>.setResult(result: Result) {
set(result)
SentryLog.d(TAG, "Work finished")
}

@Singleton
class Scheduler @Inject constructor(
private val playServicesUtils: PlayServicesUtils,
private val workManager: WorkManager,
private val localSettings: LocalSettings,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) {

suspend fun scheduleWorkIfNeeded() = withContext(ioDispatcher) {
if (playServicesUtils.areGooglePlayServicesAvailable() && localSettings.hasAppUpdateDownloaded) {
SentryLog.d(TAG, "Work scheduled")

val workRequest = OneTimeWorkRequestBuilder<AppUpdateWorker>()
.setConstraints(Constraints.Builder().setRequiresBatteryNotLow(true).build())
.build()

workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, workRequest)
}
}

fun cancelWork() {
SentryLog.d(TAG, "Work cancelled")
workManager.cancelUniqueWork(TAG)
}
}

companion object {
private const val TAG = "AppUpdateWorker"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class SyncMailboxesWorker @AssistedInject constructor(

suspend fun scheduleWorkIfNeeded() = withContext(ioDispatcher) {

if (playServicesUtils.areGooglePlayServicesNotAvailable() && AccountUtils.getAllUsersCount() > 0) {
if (!playServicesUtils.areGooglePlayServicesAvailable() && AccountUtils.getAllUsersCount() > 0) {
SentryLog.d(TAG, "Work scheduled")

val workRequest =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ class StandardMainApplication : MainApplication() {
}

private fun registerUserDeviceIfNeeded() {
val areGooglePlayServicesAvailable = !playServicesUtils.areGooglePlayServicesNotAvailable()
if (AccountUtils.currentUserId != AppSettings.DEFAULT_ID && areGooglePlayServicesAvailable && localSettings.firebaseToken != null) {
if (AccountUtils.currentUserId != AppSettings.DEFAULT_ID &&
playServicesUtils.areGooglePlayServicesAvailable() &&
localSettings.firebaseToken != null
) {
hiltEntryPoint.registerUserDeviceWorkerScheduler().scheduleWork()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ class StandardPlayServicesUtils @Inject constructor(
}
}

override fun areGooglePlayServicesNotAvailable(): Boolean {
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext) != ConnectionResult.SUCCESS
override fun areGooglePlayServicesAvailable(): Boolean {
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext) == ConnectionResult.SUCCESS
}

override fun deleteFirebaseToken() {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ realmKotlin = "1.12.0"
sentryAndroidFragment = "6.29.0"
testRunner = "1.5.2"
webkit = "1.8.0"
workConcurrentFutures = "1.1.0"
workRuntimeKtx = "2.8.1"

[libraries]
Expand All @@ -39,6 +40,7 @@ navigation-safeargs = { module = "androidx.navigation:navigation-safe-args-gradl
realm-kotlin-base = { module = "io.realm.kotlin:library-base", version.ref = "realmKotlin" }
sentry-android-fragment = { module = "io.sentry:sentry-android-fragment", version.ref = "sentryAndroidFragment" }
webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
work-concurrent-futures = { module = "androidx.concurrent:concurrent-futures-ktx", version.ref = "workConcurrentFutures" }
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
# Unit tests
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitJupiterEngine" }
Expand Down

0 comments on commit edcfd4a

Please sign in to comment.