diff --git a/app/src/main/java/illyan/jay/data/disk/dao/PreferencesDao.kt b/app/src/main/java/illyan/jay/data/disk/dao/PreferencesDao.kt index 9f723a37..ffb46b89 100644 --- a/app/src/main/java/illyan/jay/data/disk/dao/PreferencesDao.kt +++ b/app/src/main/java/illyan/jay/data/disk/dao/PreferencesDao.kt @@ -47,6 +47,6 @@ interface PreferencesDao { @Query("UPDATE preferences SET freeDriveAutoStart = :freeDriveAutoStart, lastUpdate = :lastUpdate WHERE userUUID IS :userUUID") fun setFreeDriveAutoStart(userUUID: String, freeDriveAutoStart: Boolean, lastUpdate: Long = Instant.now().toEpochMilli()) - @Query("UPDATE preferences SET shouldSync = :shouldSync, lastUpdate = :lastUpdate WHERE userUUID IS :userUUID") - fun setShouldSync(userUUID: String, shouldSync: Boolean, lastUpdate: Long = Instant.now().toEpochMilli()) + @Query("UPDATE preferences SET shouldSync = :shouldSync WHERE userUUID IS :userUUID") + fun setShouldSync(userUUID: String, shouldSync: Boolean) } \ No newline at end of file diff --git a/app/src/main/java/illyan/jay/data/network/datasource/PreferencesNetworkDataSource.kt b/app/src/main/java/illyan/jay/data/network/datasource/PreferencesNetworkDataSource.kt index 81d8e51f..0c0cd7f6 100644 --- a/app/src/main/java/illyan/jay/data/network/datasource/PreferencesNetworkDataSource.kt +++ b/app/src/main/java/illyan/jay/data/network/datasource/PreferencesNetworkDataSource.kt @@ -56,10 +56,23 @@ class PreferencesNetworkDataSource @Inject constructor( } else if (loading) { null } else { - // TODO: maybe insert settings - // possible states: - // - user not signed in: tell user they cannot sync settings? - // - user does not have settings yet + null + } + }.stateIn(coroutineScopeIO, SharingStarted.Eagerly, null) + } + + val cloudPreferences: StateFlow by lazy { + combine( + userNetworkDataSource.cloudUser, + userNetworkDataSource.isLoadingFromCloud + ) { user, loading -> + if (user?.preferences != null) { + val preferences = user.preferences.toDomainModel(userUUID = user.uuid) + Timber.d("Firebase got cloud preferences for user ${user.uuid.take(4)}") + preferences + } else if (loading) { + null + } else { null } }.stateIn(coroutineScopeIO, SharingStarted.Eagerly, null) diff --git a/app/src/main/java/illyan/jay/data/network/datasource/UserNetworkDataSource.kt b/app/src/main/java/illyan/jay/data/network/datasource/UserNetworkDataSource.kt index 6fc6c141..5d83dc6d 100644 --- a/app/src/main/java/illyan/jay/data/network/datasource/UserNetworkDataSource.kt +++ b/app/src/main/java/illyan/jay/data/network/datasource/UserNetworkDataSource.kt @@ -25,6 +25,7 @@ import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.EventListener import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.ListenerRegistration +import com.google.firebase.firestore.MetadataChanges import com.google.firebase.firestore.ktx.toObject import illyan.jay.data.network.model.FirestoreUser import illyan.jay.domain.interactor.AuthInteractor @@ -34,7 +35,9 @@ import kotlinx.coroutines.flow.asStateFlow import timber.log.Timber import java.util.concurrent.Executors import javax.inject.Inject +import javax.inject.Singleton +@Singleton class UserNetworkDataSource @Inject constructor( private val firestore: FirebaseFirestore, private val authInteractor: AuthInteractor, @@ -43,9 +46,21 @@ class UserNetworkDataSource @Inject constructor( private val _userListenerRegistration = MutableStateFlow(null) private val _userReference = MutableStateFlow(null) private val _user = MutableStateFlow(null) - val user: StateFlow get() { - if (_userListenerRegistration.value == null) loadUser() - return _user.asStateFlow() + private val _cloudUser = MutableStateFlow(null) + + val user: StateFlow by lazy { + if (_userListenerRegistration.value == null && !isLoading.value) { + Timber.d("User StateFlow requested, but listener registration is null, reloading it") + refreshUser() + } + _user.asStateFlow() + } + val cloudUser: StateFlow by lazy { + if (_userListenerRegistration.value == null && !isLoadingFromCloud.value) { + Timber.d("User StateFlow requested, but listener registration is null, reloading it") + refreshUser() + } + _cloudUser.asStateFlow() } private val _isLoading = MutableStateFlow(false) @@ -59,41 +74,53 @@ class UserNetworkDataSource @Inject constructor( init { authInteractor.addAuthStateListener { if (authInteractor.isUserSignedIn) { - Timber.d("Reloading snapshot listener for user ${_user.value?.uuid}") - loadUser() + if (authInteractor.userUUID != null && + authInteractor.userUUID != _user.value?.uuid + ) { + Timber.d("Reloading snapshot listener for user ${_user.value?.uuid?.take(4)}") + refreshUser() + } else { + Timber.d("User not changed from ${_user.value?.uuid?.take(4)}, not reloading snapshot listener on auth state change") + } } else { - Timber.d("Removing snapshot listener for user ${_user.value?.uuid}") - _userReference.value = null - _userListenerRegistration.value?.remove() - _user.value = null - _isLoading.value = false - _isLoadingFromCloud.value = false + Timber.d("Removing snapshot listener for user ${_user.value?.uuid?.take(4)}") + resetUserListenerData() } } appLifecycle.addObserver(this) } + private fun resetUserListenerData() { + _userListenerRegistration.value?.remove() + if (_userListenerRegistration.value != null) _userListenerRegistration.value = null + if (_userReference.value != null) _userReference.value = null + if (_isLoading.value) _isLoading.value = false + if (_isLoadingFromCloud.value) _isLoadingFromCloud.value = false + } + override fun onStart(owner: LifecycleOwner) { super.onStart(owner) - loadUser() + Timber.d("Reload user on App Lifecycle Start") + resetUserListenerData() + refreshUser() } override fun onStop(owner: LifecycleOwner) { super.onStop(owner) - _userListenerRegistration.value?.remove() - _userListenerRegistration.value = null + Timber.d("Remove user listener on App Lifecycle Stop") + resetUserListenerData() } - private fun loadUser( + private fun refreshUser( userUUID: String = authInteractor.userUUID.toString(), onError: (Exception) -> Unit = { Timber.e(it, "Error while getting user data: ${it.message}") }, onSuccess: (FirestoreUser) -> Unit = {}, ) { - if (!authInteractor.isUserSignedIn) return - if (_user.value == null) { - _isLoading.value = true - _isLoadingFromCloud.value = true + if (!authInteractor.isUserSignedIn || _isLoadingFromCloud.value) { + Timber.d("Not refreshing user, due to another being loaded in or user is not signed in") + return } + resetUserListenerData() Timber.d("Connecting snapshot listener to Firebase to get ${userUUID.take(4)} user's data") val snapshotListener = EventListener { snapshot, error -> Timber.v("New snapshot regarding user ${userUUID.take(4)}") @@ -104,7 +131,6 @@ class UserNetworkDataSource @Inject constructor( if (user == null) { onError(NoSuchElementException("User document does not exist")) } else { - Timber.d("Firebase loaded ${userUUID.take(4)} user's data") onSuccess(user) } if (snapshot != null) { @@ -114,24 +140,45 @@ class UserNetworkDataSource @Inject constructor( // If snapshot is null, then _userReference is invalid if not null. Assign null to it. _userReference.value = null } + + // Cache if (user != null) { + if (_user.value != null) { + Timber.v("Refreshing Cached ${userUUID.take(4)} user's data") + } else { + Timber.d("Firestore loaded ${userUUID.take(4)} user's data from Cache") + } _user.value = user } else if (_user.value != null) { + _user.value = null } if (_isLoading.value) { _isLoading.value = false } + + // Cloud + if (snapshot?.metadata?.isFromCache == false) { + if (user != null) { + if (_cloudUser.value != null) { + Timber.v("Firestore loaded fresh ${userUUID.take(4)} user's data from Cloud") + } else { + Timber.d("Firestore loaded ${userUUID.take(4)} user's data from Cloud") + } + _cloudUser.value = user + } else if (_cloudUser.value != null) { + _cloudUser.value = null + } + } if (_isLoadingFromCloud.value && snapshot?.metadata?.isFromCache == false) { _isLoadingFromCloud.value = false } } } - _userListenerRegistration.value?.remove() _userListenerRegistration.value = firestore .collection(FirestoreUser.CollectionName) .document(userUUID) - .addSnapshotListener(executor, snapshotListener) + .addSnapshotListener(executor, MetadataChanges.INCLUDE, snapshotListener) } fun deleteUserData( diff --git a/app/src/main/java/illyan/jay/domain/interactor/SettingsInteractor.kt b/app/src/main/java/illyan/jay/domain/interactor/SettingsInteractor.kt index c6d9bd42..d4fb1ab3 100644 --- a/app/src/main/java/illyan/jay/domain/interactor/SettingsInteractor.kt +++ b/app/src/main/java/illyan/jay/domain/interactor/SettingsInteractor.kt @@ -106,15 +106,20 @@ class SettingsInteractor @Inject constructor( val arePreferencesSynced by lazy { combine( isLoading, + preferencesNetworkDataSource.isLoadingFromCloud, localUserPreferences, syncedUserPreferences - ) { loading, local, synced -> - if (!loading) { + ) { loading, cloudLoading, local, synced -> + if (!loading && !cloudLoading) { local == synced } else { false } - }.stateIn(coroutineScopeIO, SharingStarted.Eagerly, false) + }.stateIn( + coroutineScopeIO, + SharingStarted.Eagerly, + localUserPreferences.value == syncedUserPreferences.value + ) } val shouldSyncPreferences by lazy { @@ -144,13 +149,12 @@ class SettingsInteractor @Inject constructor( return _isLocalLoading.asStateFlow() } - val syncedUserPreferences = preferencesNetworkDataSource.preferences + val syncedUserPreferences = preferencesNetworkDataSource.cloudPreferences val isLoading = combine( preferencesNetworkDataSource.isLoading, - preferencesNetworkDataSource.isLoadingFromCloud, isLocalLoading - ) { loadingFromCache, loadingFromCloud, loadingFromDisk -> + ) { loadingFromCache, loadingFromDisk -> loadingFromCache || loadingFromDisk }.stateIn(coroutineScopeIO, SharingStarted.Eagerly, false) @@ -216,7 +220,6 @@ class SettingsInteractor @Inject constructor( Timber.v("If cloud is loaded and local is not, returning cloud preferences") syncedPreferences } else { - // TODO: resolve preferences here, local and synced preferences are loaded if (localPreferences == null && syncedPreferences == null) { // User don't have local nor synced preferences? Create and upload local preferences. Timber.v("User don't have local nor synced preferences, create and upload one.") diff --git a/app/src/main/java/illyan/jay/domain/model/DomainPreferences.kt b/app/src/main/java/illyan/jay/domain/model/DomainPreferences.kt index 0534008a..1cd16b6b 100644 --- a/app/src/main/java/illyan/jay/domain/model/DomainPreferences.kt +++ b/app/src/main/java/illyan/jay/domain/model/DomainPreferences.kt @@ -30,7 +30,7 @@ data class DomainPreferences( val freeDriveAutoStart: Boolean = false, @Serializable(with = ZonedDateTimeSerializer::class) val lastUpdate: ZonedDateTime = ZonedDateTime.now(), - val shouldSync: Boolean = false + val shouldSync: Boolean = false // FIXME: change this to true, because user preferences would break bcuz it would never download cloud preferences. Maybe make sync not update lastupdate? ) { override fun equals(other: Any?): Boolean { return if (other != null && other is DomainPreferences) {