diff --git a/Stores/build.gradle b/Stores/build.gradle index b1a221f7..5762c441 100644 --- a/Stores/build.gradle +++ b/Stores/build.gradle @@ -31,6 +31,8 @@ android { dependencies { implementation project(path: ':Core') + api 'androidx.datastore:datastore-preferences:1.0.0' + implementation 'androidx.work:work-runtime-ktx:2.9.0' implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0' diff --git a/Stores/src/main/java/com/infomaniak/lib/stores/AppUpdateWorker.kt b/Stores/src/main/java/com/infomaniak/lib/stores/AppUpdateWorker.kt index 391787a5..0fb2b984 100644 --- a/Stores/src/main/java/com/infomaniak/lib/stores/AppUpdateWorker.kt +++ b/Stores/src/main/java/com/infomaniak/lib/stores/AppUpdateWorker.kt @@ -25,7 +25,9 @@ import com.infomaniak.lib.core.utils.SentryLog import com.infomaniak.lib.stores.updatemanagers.WorkerUpdateManager import io.sentry.Sentry import io.sentry.SentryLevel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.TimeUnit @@ -56,10 +58,11 @@ class AppUpdateWorker(appContext: Context, params: WorkerParameters) : Listenabl class Scheduler(appContext: Context) { private val workManager = WorkManager.getInstance(appContext) - private val storesLocalSettings = StoresLocalSettings.getInstance(appContext) + private val storesSettingsRepository = StoresSettingsRepository(appContext) - fun scheduleWorkIfNeeded() { - if (BuildConfig.HAS_GPLAY_SERVICES && storesLocalSettings.hasAppUpdateDownloaded) { + fun scheduleWorkIfNeeded() = CoroutineScope(Dispatchers.IO).launch { + val hasAppUpdateDownloaded = storesSettingsRepository.getValue(StoresSettingsRepository.HAS_APP_UPDATE_DOWNLOADED) + if (BuildConfig.HAS_GPLAY_SERVICES && hasAppUpdateDownloaded) { SentryLog.d(TAG, "Work scheduled") val workRequest = OneTimeWorkRequestBuilder() diff --git a/Stores/src/main/java/com/infomaniak/lib/stores/BaseInAppUpdateManager.kt b/Stores/src/main/java/com/infomaniak/lib/stores/BaseInAppUpdateManager.kt index 44a75264..ed0ff154 100644 --- a/Stores/src/main/java/com/infomaniak/lib/stores/BaseInAppUpdateManager.kt +++ b/Stores/src/main/java/com/infomaniak/lib/stores/BaseInAppUpdateManager.kt @@ -20,26 +20,25 @@ package com.infomaniak.lib.stores import androidx.fragment.app.FragmentActivity import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider abstract class BaseInAppUpdateManager(activity: FragmentActivity) : DefaultLifecycleObserver { var onInAppUpdateUiChange: ((Boolean) -> Unit)? = null var onFDroidResult: ((Boolean) -> Unit)? = null - protected val localSettings = StoresLocalSettings.getInstance(activity) + protected val viewModel: StoresViewModel by lazy { ViewModelProvider(activity)[StoresViewModel::class.java] } override fun onStart(owner: LifecycleOwner) { super.onStart(owner) - handleUpdates() - localSettings.appUpdateLaunches++ + with(viewModel) { + shouldCheckUpdate(::checkUpdateIsAvailable) + incrementAppUpdateLaunches() + } } abstract fun installDownloadedUpdate() protected abstract fun checkUpdateIsAvailable() - - private fun handleUpdates() = with(localSettings) { - if (appUpdateLaunches != 0 && (isUserWantingUpdates || appUpdateLaunches % 10 == 0)) checkUpdateIsAvailable() - } } diff --git a/Stores/src/main/java/com/infomaniak/lib/stores/StoresLocalSettings.kt b/Stores/src/main/java/com/infomaniak/lib/stores/StoresLocalSettings.kt deleted file mode 100644 index db41914f..00000000 --- a/Stores/src/main/java/com/infomaniak/lib/stores/StoresLocalSettings.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2024 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 . - */ -package com.infomaniak.lib.stores - -import android.content.Context -import com.infomaniak.lib.core.utils.SharedValues -import com.infomaniak.lib.core.utils.transaction - -class StoresLocalSettings private constructor(context: Context) : SharedValues { - - override val sharedPreferences = context.applicationContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE)!! - - var isUserWantingUpdates by sharedValue("isUserWantingUpdatesKey", true) - var hasAppUpdateDownloaded by sharedValue("hasAppUpdateDownloadedKey", false) - var appUpdateLaunches by sharedValue("appUpdateLaunchesKey", 0) - var appReviewLaunches by sharedValue("appReviewLaunchesKey", DEFAULT_APP_REVIEW_LAUNCHES) - var showAppReviewDialog by sharedValue("showAppReviewDialogKey", true) - - fun removeSettings() = sharedPreferences.transaction { clear() } - - fun resetUpdateSettings() { - isUserWantingUpdates = false // This avoid the user being instantly reprompted to download update - hasAppUpdateDownloaded = false - } - - companion object { - - const val DEFAULT_APP_REVIEW_LAUNCHES = 50 - - private const val SHARED_PREFS_NAME = "StoresLocalSettingsSharedPref" - - @Volatile - private var INSTANCE: StoresLocalSettings? = null - - fun getInstance(context: Context): StoresLocalSettings { - return INSTANCE ?: synchronized(this) { - INSTANCE?.let { return it } - StoresLocalSettings(context).also { INSTANCE = it } - } - } - } -} diff --git a/Stores/src/main/java/com/infomaniak/lib/stores/StoresSettingsRepository.kt b/Stores/src/main/java/com/infomaniak/lib/stores/StoresSettingsRepository.kt new file mode 100644 index 00000000..0de1c242 --- /dev/null +++ b/Stores/src/main/java/com/infomaniak/lib/stores/StoresSettingsRepository.kt @@ -0,0 +1,89 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2024 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 . + */ +package com.infomaniak.lib.stores + +import android.content.Context +import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import java.io.IOException + +private val Context.dataStore by preferencesDataStore(name = StoresSettingsRepository.DATA_STORE_NAME) + +class StoresSettingsRepository(private val context: Context) { + + val flow: Flow = context.dataStore.data + .catch { if (it is IOException) emit(emptyPreferences()) else throw it } + .map { it.toStoresSettings() } + + @Suppress("UNCHECKED_CAST") + fun flowOf(key: Preferences.Key): Flow { + val defaultValue = when (key) { + IS_USER_WANTING_UPDATES -> DEFAULT_IS_USER_WANTING_UPDATES + HAS_APP_UPDATE_DOWNLOADED -> DEFAULT_HAS_APP_UPDATE_DOWNLOADED + APP_UPDATE_LAUNCHES -> DEFAULT_APP_UPDATE_LAUNCHES + else -> throw IllegalArgumentException("Unknown Preferences.Key") + } + + return context.dataStore.data.map { it[key] ?: (defaultValue as T) } + } + + suspend fun getAll() = context.dataStore.data.first().toStoresSettings() + + suspend fun getValue(key: Preferences.Key): T = flowOf(key).first() + + suspend fun setValue(key: Preferences.Key, value: T) { + context.dataStore.edit { it[key] = value } + } + + suspend fun clear() = context.dataStore.edit(MutablePreferences::clear) + + suspend fun resetUpdateSettings() { + // This avoid the user being instantly reprompted to download update + setValue(IS_USER_WANTING_UPDATES, false) + setValue(HAS_APP_UPDATE_DOWNLOADED, false) + } + + private fun Preferences.toStoresSettings() = StoresSettings( + isUserWantingUpdates = this[IS_USER_WANTING_UPDATES] ?: DEFAULT_IS_USER_WANTING_UPDATES, + hasAppUpdateDownloaded = this[HAS_APP_UPDATE_DOWNLOADED] ?: DEFAULT_HAS_APP_UPDATE_DOWNLOADED, + appUpdateLaunches = this[APP_UPDATE_LAUNCHES] ?: DEFAULT_APP_UPDATE_LAUNCHES, + ) + + data class StoresSettings( + val isUserWantingUpdates: Boolean, + val hasAppUpdateDownloaded: Boolean, + val appUpdateLaunches: Int, + ) + + companion object { + + val IS_USER_WANTING_UPDATES = booleanPreferencesKey("isUserWantingUpdatesKey") + val HAS_APP_UPDATE_DOWNLOADED = booleanPreferencesKey("hasAppUpdateDownloadedKey") + val APP_UPDATE_LAUNCHES = intPreferencesKey("appUpdateLaunchesKey") + + const val DATA_STORE_NAME = "StoresSettingsDataStore" + + private const val DEFAULT_IS_USER_WANTING_UPDATES = true + private const val DEFAULT_HAS_APP_UPDATE_DOWNLOADED = false + private const val DEFAULT_APP_UPDATE_LAUNCHES = 0 + } +} diff --git a/Stores/src/main/java/com/infomaniak/lib/stores/StoresViewModel.kt b/Stores/src/main/java/com/infomaniak/lib/stores/StoresViewModel.kt index 1a8c5354..fc8ce931 100644 --- a/Stores/src/main/java/com/infomaniak/lib/stores/StoresViewModel.kt +++ b/Stores/src/main/java/com/infomaniak/lib/stores/StoresViewModel.kt @@ -17,17 +17,41 @@ */ package com.infomaniak.lib.stores -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.infomaniak.lib.core.utils.SentryLog +import android.app.Application +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.lifecycle.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch -internal class StoresViewModel : ViewModel() { +class StoresViewModel(application: Application) : AndroidViewModel(application) { - val canInstallUpdate = MutableLiveData(false) + private inline val context: Context get() = getApplication() + private val ioCoroutineContext = viewModelScope.coroutineContext + Dispatchers.IO + private val storesSettingsRepository = StoresSettingsRepository(context) - fun toggleAppUpdateStatus(localSettings: StoresLocalSettings, isUpdateDownloaded: Boolean) { - SentryLog.d(StoreUtils.APP_UPDATE_TAG, "Setting canInstallUpdate value to $isUpdateDownloaded in toggleAppUpdateStatus") - canInstallUpdate.value = isUpdateDownloaded - localSettings.hasAppUpdateDownloaded = isUpdateDownloaded + val storesSettingsLiveData = storesSettingsRepository.flow.asLiveData(ioCoroutineContext) + + fun liveDataOf(key: Preferences.Key): LiveData { + return storesSettingsRepository.flowOf(key).asLiveData(ioCoroutineContext).distinctUntilChanged() + } + + fun set(key: Preferences.Key, value: T) = viewModelScope.launch(ioCoroutineContext) { + storesSettingsRepository.setValue(key, value) + } + + fun resetUpdateSettings() = viewModelScope.launch(ioCoroutineContext) { storesSettingsRepository.resetUpdateSettings() } + + //region BaseInAppUpdateManager + fun incrementAppUpdateLaunches() = viewModelScope.launch(ioCoroutineContext) { + val appUpdateLaunches = storesSettingsRepository.getValue(StoresSettingsRepository.APP_UPDATE_LAUNCHES) + set(StoresSettingsRepository.APP_UPDATE_LAUNCHES, appUpdateLaunches + 1) + } + + fun shouldCheckUpdate(checkUpdateCallback: () -> Unit) = viewModelScope.launch(ioCoroutineContext) { + with(storesSettingsRepository.getAll()) { + if (appUpdateLaunches != 0 && (isUserWantingUpdates || appUpdateLaunches % 10 == 0)) checkUpdateCallback() + } } + //endregion } diff --git a/Stores/src/standard/java/com/infomaniak/lib/stores/updatemanagers/InAppUpdateManager.kt b/Stores/src/standard/java/com/infomaniak/lib/stores/updatemanagers/InAppUpdateManager.kt index 66c0c885..5ac8ef59 100644 --- a/Stores/src/standard/java/com/infomaniak/lib/stores/updatemanagers/InAppUpdateManager.kt +++ b/Stores/src/standard/java/com/infomaniak/lib/stores/updatemanagers/InAppUpdateManager.kt @@ -23,7 +23,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModelProvider import com.google.android.play.core.appupdate.AppUpdateInfo import com.google.android.play.core.appupdate.AppUpdateManagerFactory import com.google.android.play.core.appupdate.AppUpdateOptions @@ -33,7 +32,7 @@ import com.google.android.play.core.install.model.UpdateAvailability import com.infomaniak.lib.core.utils.SentryLog import com.infomaniak.lib.stores.BaseInAppUpdateManager import com.infomaniak.lib.stores.StoreUtils -import com.infomaniak.lib.stores.StoresViewModel +import com.infomaniak.lib.stores.StoresSettingsRepository class InAppUpdateManager( private val activity: FragmentActivity, @@ -51,14 +50,12 @@ class InAppUpdateManager( ActivityResultContracts.StartIntentSenderForResult() ) { result -> val isUserWantingUpdate = result.resultCode == AppCompatActivity.RESULT_OK - localSettings.isUserWantingUpdates = isUserWantingUpdate + viewModel.set(StoresSettingsRepository.IS_USER_WANTING_UPDATES, isUserWantingUpdate) onUserChoice(isUserWantingUpdate) } - private val viewModel: StoresViewModel by lazy { ViewModelProvider(activity)[StoresViewModel::class.java] } - - private val onUpdateDownloaded = { viewModel.toggleAppUpdateStatus(localSettings, isUpdateDownloaded = true) } - private val onUpdateInstalled = { viewModel.toggleAppUpdateStatus(localSettings, isUpdateDownloaded = false) } + private val onUpdateDownloaded = { viewModel.set(StoresSettingsRepository.HAS_APP_UPDATE_DOWNLOADED, true) } + private val onUpdateInstalled = { viewModel.set(StoresSettingsRepository.HAS_APP_UPDATE_DOWNLOADED, false) } // Create a listener to track request state updates. private val installStateUpdatedListener by lazy { @@ -85,8 +82,6 @@ class InAppUpdateManager( override fun onResume(owner: LifecycleOwner) { super.onResume(owner) - - viewModel.canInstallUpdate.value = localSettings.hasAppUpdateDownloaded checkStalledUpdate() } @@ -109,19 +104,20 @@ class InAppUpdateManager( } override fun installDownloadedUpdate() { - localSettings.hasAppUpdateDownloaded = false - viewModel.canInstallUpdate.value = false + viewModel.set(StoresSettingsRepository.HAS_APP_UPDATE_DOWNLOADED, false) onInstallStart() appUpdateManager.completeUpdate() .addOnSuccessListener { onInstallSuccess?.invoke() } .addOnFailureListener { - localSettings.resetUpdateSettings() + viewModel.resetUpdateSettings() onInstallFailure(it) } } private fun observeAppUpdateDownload() { - viewModel.canInstallUpdate.observe(activity) { isUpdateDownloaded -> onInAppUpdateUiChange?.invoke(isUpdateDownloaded) } + viewModel.liveDataOf(StoresSettingsRepository.HAS_APP_UPDATE_DOWNLOADED).observe(activity) { + onInAppUpdateUiChange?.invoke(it) + } } private fun checkStalledUpdate(): Unit = with(appUpdateManager) { diff --git a/Stores/src/standard/java/com/infomaniak/lib/stores/updatemanagers/WorkerUpdateManager.kt b/Stores/src/standard/java/com/infomaniak/lib/stores/updatemanagers/WorkerUpdateManager.kt index f92aef63..a2f1048e 100644 --- a/Stores/src/standard/java/com/infomaniak/lib/stores/updatemanagers/WorkerUpdateManager.kt +++ b/Stores/src/standard/java/com/infomaniak/lib/stores/updatemanagers/WorkerUpdateManager.kt @@ -19,19 +19,24 @@ package com.infomaniak.lib.stores.updatemanagers import android.content.Context import com.google.android.play.core.appupdate.AppUpdateManagerFactory -import com.infomaniak.lib.stores.StoresLocalSettings +import com.infomaniak.lib.stores.StoresSettingsRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch internal class WorkerUpdateManager(appContext: Context) { private val appUpdateManager = AppUpdateManagerFactory.create(appContext) - private val localSettings = StoresLocalSettings.getInstance(appContext) + private val storesSettingsRepository = StoresSettingsRepository(appContext) fun installDownloadedUpdate(onInstallFailure: (Exception) -> Unit, onInstallSuccess: () -> Unit) { - localSettings.hasAppUpdateDownloaded = false + CoroutineScope(Dispatchers.IO).launch { + storesSettingsRepository.setValue(StoresSettingsRepository.HAS_APP_UPDATE_DOWNLOADED, false) + } appUpdateManager.completeUpdate() .addOnSuccessListener { onInstallSuccess() } .addOnFailureListener { - localSettings.resetUpdateSettings() + CoroutineScope(Dispatchers.IO).launch { storesSettingsRepository.resetUpdateSettings() } onInstallFailure(it) } }