Skip to content

Commit

Permalink
Merge pull request #63 from HLCaptain/feature-compose
Browse files Browse the repository at this point in the history
Fixed issues with syncing
  • Loading branch information
HLCaptain authored Feb 26, 2023
2 parents a0edcd1 + 3f87f1c commit 5e779b7
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 69 deletions.
4 changes: 2 additions & 2 deletions app/src/main/java/illyan/jay/data/disk/dao/PreferencesDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package illyan.jay.data.disk.model

import androidx.room.Entity
import androidx.room.PrimaryKey
import illyan.jay.domain.model.DomainPreferences
import java.time.ZonedDateTime
import java.util.UUID

Expand All @@ -29,8 +30,8 @@ import java.util.UUID
data class RoomPreferences(
@PrimaryKey
val userUUID: String = UUID.randomUUID().toString(),
val freeDriveAutoStart: Boolean = false,
val analyticsEnabled: Boolean = false,
val freeDriveAutoStart: Boolean = DomainPreferences.default.freeDriveAutoStart,
val analyticsEnabled: Boolean = DomainPreferences.default.analyticsEnabled,
val lastUpdate: Long = ZonedDateTime.now().toInstant().toEpochMilli(),
val shouldSync: Boolean = false
val shouldSync: Boolean = DomainPreferences.default.shouldSync
)
Original file line number Diff line number Diff line change
Expand Up @@ -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<DomainPreferences?> 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)
Expand All @@ -73,7 +86,7 @@ class PreferencesNetworkDataSource @Inject constructor(
val userRef = firestore
.collection(FirestoreUser.CollectionName)
.document(authInteractor.userUUID!!)
val fieldMapToSet = mapOf(FirestoreUser.FieldSettings to preferences.toFirestoreModel())
val fieldMapToSet = mapOf(FirestoreUser.FieldPreferences to preferences.toFirestoreModel())
batch.set(
userRef,
fieldMapToSet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -43,9 +46,21 @@ class UserNetworkDataSource @Inject constructor(
private val _userListenerRegistration = MutableStateFlow<ListenerRegistration?>(null)
private val _userReference = MutableStateFlow<DocumentSnapshot?>(null)
private val _user = MutableStateFlow<FirestoreUser?>(null)
val user: StateFlow<FirestoreUser?> get() {
if (_userListenerRegistration.value == null) loadUser()
return _user.asStateFlow()
private val _cloudUser = MutableStateFlow<FirestoreUser?>(null)

val user: StateFlow<FirestoreUser?> 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<FirestoreUser?> 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)
Expand All @@ -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<DocumentSnapshot> { snapshot, error ->
Timber.v("New snapshot regarding user ${userUUID.take(4)}")
Expand All @@ -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) {
Expand All @@ -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(
Expand Down
31 changes: 16 additions & 15 deletions app/src/main/java/illyan/jay/data/network/model/FirestorePath.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import android.os.Parcelable
import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentId
import com.google.firebase.firestore.GeoPoint
import com.google.firebase.firestore.PropertyName
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler

Expand All @@ -31,21 +32,21 @@ import kotlinx.parcelize.TypeParceler
data class FirestorePath(
@DocumentId
val uuid: String = "",
val sessionUUID: String = "", // reference of the session this path is part of
val ownerUUID: String = "",
val accuracyChangeTimestamps: List<Timestamp> = emptyList(),
val accuracyChanges: List<Int> = emptyList(),
val altitudes: List<Int> = emptyList(),
val bearingAccuracyChangeTimestamps: List<Timestamp> = emptyList(),
val bearingAccuracyChanges: List<Int> = emptyList(),
val bearings: List<Int> = emptyList(),
val coords: List<GeoPoint> = emptyList(),
val speeds: List<Float> = emptyList(),
val speedAccuracyChangeTimestamps: List<Timestamp> = emptyList(),
val speedAccuracyChanges: List<Float> = emptyList(),
val timestamps: List<Timestamp> = emptyList(),
val verticalAccuracyChangeTimestamps: List<Timestamp> = emptyList(),
val verticalAccuracyChanges: List<Int> = emptyList()
@PropertyName(FieldSessionUUID) val sessionUUID: String = "", // reference of the session this path is part of
@PropertyName(FieldOwnerUUID) val ownerUUID: String = "",
@PropertyName(FieldAccuracyChangeTimestamps) val accuracyChangeTimestamps: List<Timestamp> = emptyList(),
@PropertyName(FieldAccuracyChanges) val accuracyChanges: List<Int> = emptyList(),
@PropertyName(FieldAltitudes) val altitudes: List<Int> = emptyList(),
@PropertyName(FieldBearingAccuracyChangeTimestamps) val bearingAccuracyChangeTimestamps: List<Timestamp> = emptyList(),
@PropertyName(FieldBearingAccuracyChanges) val bearingAccuracyChanges: List<Int> = emptyList(),
@PropertyName(FieldBearings) val bearings: List<Int> = emptyList(),
@PropertyName(FieldCoords) val coords: List<GeoPoint> = emptyList(),
@PropertyName(FieldSpeeds) val speeds: List<Float> = emptyList(),
@PropertyName(FieldSpeedAccuracyChangeTimestamps) val speedAccuracyChangeTimestamps: List<Timestamp> = emptyList(),
@PropertyName(FieldSpeedAccuracyChanges) val speedAccuracyChanges: List<Float> = emptyList(),
@PropertyName(FieldTimestamps) val timestamps: List<Timestamp> = emptyList(),
@PropertyName(FieldVerticalAccuracyChangeTimestamps) val verticalAccuracyChangeTimestamps: List<Timestamp> = emptyList(),
@PropertyName(FieldVerticalAccuracyChanges) val verticalAccuracyChanges: List<Int> = emptyList()
) : Parcelable {
companion object {
const val CollectionName = "paths"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,20 @@ package illyan.jay.data.network.model

import com.google.firebase.Timestamp
import com.google.firebase.firestore.GeoPoint
import com.google.firebase.firestore.PropertyName
import illyan.jay.util.toTimestamp
import java.time.Instant

data class FirestoreSession(
val uuid: String = "",
val startDateTime: Timestamp = Instant.EPOCH.toTimestamp(),
val endDateTime: Timestamp? = null,
val startLocation: GeoPoint? = null,
val endLocation: GeoPoint? = null,
val startLocationName: String? = null,
val endLocationName: String? = null,
val distance: Float? = null,
val clientUUID: String? = null
@PropertyName(FieldUUID) val uuid: String = "",
@PropertyName(FieldStartDateTime) val startDateTime: Timestamp = Instant.EPOCH.toTimestamp(),
@PropertyName(FieldEndDateTime) val endDateTime: Timestamp? = null,
@PropertyName(FieldStartLocation) val startLocation: GeoPoint? = null,
@PropertyName(FieldEndLocation) val endLocation: GeoPoint? = null,
@PropertyName(FieldStartLocationName) val startLocationName: String? = null,
@PropertyName(FieldEndLocationName) val endLocationName: String? = null,
@PropertyName(FieldDistance) val distance: Float? = null,
@PropertyName(FieldClientUUID) val clientUUID: String? = null
) {
companion object {
const val FieldUUID = "uuid"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@
package illyan.jay.data.network.model

import com.google.firebase.firestore.DocumentId
import com.google.firebase.firestore.PropertyName

data class FirestoreUser(
@DocumentId
val uuid: String = "",
val sessions: List<FirestoreSession> = emptyList(),
val preferences: FirestoreUserPreferences? = null
@PropertyName(FieldSessions) val sessions: List<FirestoreSession> = emptyList(),
@PropertyName(FieldPreferences) val preferences: FirestoreUserPreferences? = null
) {
companion object {
const val CollectionName = "users"
const val FieldSessions = "sessions"
const val FieldSettings = "preferences"
const val FieldPreferences = "preferences"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@
package illyan.jay.data.network.model

import com.google.firebase.Timestamp
import com.google.firebase.firestore.PropertyName
import illyan.jay.domain.model.DomainPreferences

data class FirestoreUserPreferences(
val analyticsEnabled: Boolean = false,
val freeDriveAutoStart: Boolean = false,
val lastUpdate: Timestamp = Timestamp.now()
@PropertyName(FieldAnalyticsEnabled) val analyticsEnabled: Boolean = DomainPreferences.default.analyticsEnabled,
@PropertyName(FieldFreeDriveAutoStart) val freeDriveAutoStart: Boolean = DomainPreferences.default.freeDriveAutoStart,
@PropertyName(FieldLastUpdate) val lastUpdate: Timestamp = Timestamp.now()
) {
companion object {
const val FieldAnalyticsEnabled = "analyticsEnabled"
const val FieldFreeDriveAutoStart = "freeDriveAutoStart"
const val FieldLastUpdate = "lastUpdate"
}
}
Loading

0 comments on commit 5e779b7

Please sign in to comment.