Skip to content

Commit

Permalink
Replace SharedPrefs by dataStores
Browse files Browse the repository at this point in the history
  • Loading branch information
FabianDevel committed Feb 7, 2024
1 parent b652fd5 commit 70c0380
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 93 deletions.
2 changes: 2 additions & 0 deletions Stores/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

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

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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<StoresSettings> = context.dataStore.data
.catch { if (it is IOException) emit(emptyPreferences()) else throw it }
.map { it.toStoresSettings() }

@Suppress("UNCHECKED_CAST")
fun <T> flowOf(key: Preferences.Key<T>): Flow<T> {
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 <T> getValue(key: Preferences.Key<T>): T = flowOf(key).first()

suspend fun <T> setValue(key: Preferences.Key<T>, 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
}
}
42 changes: 33 additions & 9 deletions Stores/src/main/java/com/infomaniak/lib/stores/StoresViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> liveDataOf(key: Preferences.Key<T>): LiveData<T> {
return storesSettingsRepository.flowOf(key).asLiveData(ioCoroutineContext).distinctUntilChanged()
}

fun <T> set(key: Preferences.Key<T>, 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -85,8 +82,6 @@ class InAppUpdateManager(

override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)

viewModel.canInstallUpdate.value = localSettings.hasAppUpdateDownloaded
checkStalledUpdate()
}

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down

0 comments on commit 70c0380

Please sign in to comment.