Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,12 @@ class AppPreferencesRepository(private val preferencesHelper: PreferencesHelper)
override fun setIfLanguageIsVietnamese(isVietnamese: Boolean) {
preferencesHelper.setIfLanguageIsVietnamese(isVietnamese)
}

override fun getLastKnownLocation(): Pair<Double, Double>? {
return preferencesHelper.getLastKnownLocation()
}

override fun setLastKnownLocation(lat: Double, lng: Double) {
preferencesHelper.setLastKnownLocation(lat, lng)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ interface PreferencesRepository {
fun getIfLanguageIsVietnamese(): Boolean

fun setIfLanguageIsVietnamese(isVietnamese: Boolean)

fun getLastKnownLocation(): Pair<Double, Double>?

fun setLastKnownLocation(lat: Double, lng: Double)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.iceteaviet.fastfoodfinder.data.domain.routing

import com.iceteaviet.fastfoodfinder.data.remote.routing.MapsRoutingApiHelper
import com.iceteaviet.fastfoodfinder.data.remote.routing.model.MapsDirection
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Store
import com.iceteaviet.fastfoodfinder.domain.model.Store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.iceteaviet.fastfoodfinder.data.domain.routing

import com.iceteaviet.fastfoodfinder.data.remote.routing.model.MapsDirection
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Store
import com.iceteaviet.fastfoodfinder.domain.model.Store

interface MapsRoutingRepository {
suspend fun getMapsDirection(queries: Map<String, String>, store: Store): MapsDirection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,63 @@ import com.iceteaviet.fastfoodfinder.data.local.db.store.model.toEntity
import com.iceteaviet.fastfoodfinder.data.local.db.store.model.toDomain
import com.iceteaviet.fastfoodfinder.data.remote.store.StoreApiHelper
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Comment
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Store
import com.iceteaviet.fastfoodfinder.domain.model.Store
import com.iceteaviet.fastfoodfinder.utils.exception.NotFoundException
import com.iceteaviet.fastfoodfinder.utils.getStoreTypeFromQuery
import com.iceteaviet.fastfoodfinder.utils.standardizeDistrictQuery
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext

class AppStoreRepository(private val storeApiHelper: StoreApiHelper, private val storeDao: StoreDao) : StoreRepository {

@VisibleForTesting
private var cachedStores: List<Store> = ArrayList()
internal var cachedStores: List<Store> = ArrayList()

private val cacheMutex = Mutex()

override suspend fun getAllStores(): List<Store> = withContext(Dispatchers.IO) {
if (cachedStores.isNotEmpty()) {
return@withContext ArrayList(cachedStores)
cacheMutex.withLock {
if (cachedStores.isNotEmpty()) {
return@withContext ArrayList(cachedStores)
}

val localStores = storeDao.getAllStores().map { it.toDomain() }
if (localStores.isNotEmpty()) {
cachedStores = localStores
return@withContext ArrayList(localStores)
}

val remoteStores = storeApiHelper.getAllStores()
val filtered = filterInvalidData(remoteStores)
persistStoresLocked(filtered)
ArrayList(filtered)
}
}

val localStores = storeDao.getAllStores().map { it.toDomain() }
if (localStores.isNotEmpty()) {
cachedStores = localStores
return@withContext ArrayList(localStores)
override suspend fun refreshStores(): List<Store> = withContext(Dispatchers.IO) {
cacheMutex.withLock {
val remoteStores = storeApiHelper.getAllStores()
val filtered = filterInvalidData(remoteStores)
persistStoresLocked(filtered)
ArrayList(filtered)
}

val remoteStores = storeApiHelper.getAllStores()
cachedStores = remoteStores
ArrayList(remoteStores)
}

override suspend fun setStores(storeList: List<Store>) = withContext(Dispatchers.IO) {
cachedStores = storeList
storeDao.insertAll(storeList.map { it.toEntity() })
cacheMutex.withLock {
persistStoresLocked(storeList)
}
}

private suspend fun persistStoresLocked(stores: List<Store>) {
cachedStores = stores
if (stores.isNotEmpty()) {
storeDao.insertAll(stores.map { it.toEntity() })
} else {
storeDao.deleteAll()
}
}

override suspend fun getStoreInBounds(lat: Double, lng: Double, radius: Double): List<Store> = withContext(Dispatchers.IO) {
Expand All @@ -57,22 +83,6 @@ class AppStoreRepository(private val storeApiHelper: StoreApiHelper, private val
customQuerySearch.flatMap { storeDao.findStoresByCustomAddress(it).map { e -> e.toDomain() } }.distinctBy { it.id }
}

override suspend fun findStoresBy(key: String, value: Int): List<Store> = withContext(Dispatchers.IO) {
when (key) {
"type" -> storeDao.findStoresByType(value).map { it.toDomain() }
"id" -> storeDao.findStoreById(value)?.let { listOf(it.toDomain()) } ?: emptyList()
else -> emptyList()
}
}

override suspend fun findStoresBy(key: String, values: List<Int>): List<Store> = withContext(Dispatchers.IO) {
when (key) {
"id" -> storeDao.findStoresByIds(values).map { it.toDomain() }
"type" -> values.flatMap { storeDao.findStoresByType(it).map { e -> e.toDomain() } }.distinctBy { it.id }
else -> emptyList()
}
}

override suspend fun findStoresByType(type: Int): List<Store> = withContext(Dispatchers.IO) {
storeDao.findStoresByType(type).map { it.toDomain() }
}
Expand All @@ -86,8 +96,10 @@ class AppStoreRepository(private val storeApiHelper: StoreApiHelper, private val
}

override suspend fun deleteAllStores() = withContext(Dispatchers.IO) {
cachedStores = ArrayList()
storeDao.deleteAll()
cacheMutex.withLock {
cachedStores = ArrayList()
storeDao.deleteAll()
}
}

override suspend fun getComments(storeId: String): List<Comment> = withContext(Dispatchers.IO) {
Expand All @@ -98,7 +110,16 @@ class AppStoreRepository(private val storeApiHelper: StoreApiHelper, private val
storeApiHelper.insertOrUpdateComment(storeId, comment)
}

fun clearCache() {
cachedStores = ArrayList()
suspend fun clearCache() {
cacheMutex.withLock {
cachedStores = ArrayList()
}
}

@VisibleForTesting
internal fun filterInvalidData(stores: List<Store>): List<Store> {
return stores.filter { store ->
store.id >= 0 && store.lat in 0.0..90.0 && store.lng in 0.0..180.0 && store.address.isNotBlank()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.iceteaviet.fastfoodfinder.data.domain.store

import com.iceteaviet.fastfoodfinder.data.auth.ClientAuth
import com.iceteaviet.fastfoodfinder.domain.model.Store
import javax.inject.Inject

class StoreRefreshService @Inject constructor(
private val storeRepository: StoreRepository,
private val clientAuth: ClientAuth,
private val botEmail: String,
private val botPassword: String,
) {
suspend fun refreshStoresFromRemote(): List<Store> {
if (clientAuth.isSignedIn()) {
return storeRepository.refreshStores()
}

clientAuth.signInWithEmailAndPassword(botEmail, botPassword)
try {
return storeRepository.refreshStores()
} finally {
clientAuth.signOut()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.iceteaviet.fastfoodfinder.data.domain.store

import com.iceteaviet.fastfoodfinder.data.remote.store.model.Comment
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Store
import com.iceteaviet.fastfoodfinder.domain.model.Store

interface StoreRepository {

suspend fun getAllStores(): List<Store>

suspend fun refreshStores(): List<Store>

suspend fun setStores(storeList: List<Store>)

suspend fun getStoreInBounds(lat: Double, lng: Double, radius: Double): List<Store>
Expand All @@ -15,10 +17,6 @@ interface StoreRepository {

suspend fun findStoresByCustomAddress(customQuerySearch: List<String>): List<Store>

suspend fun findStoresBy(key: String, value: Int): List<Store>

suspend fun findStoresBy(key: String, values: List<Int>): List<Store>

suspend fun findStoresByType(type: Int): List<Store>

suspend fun findStoreById(id: Int): Store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.iceteaviet.fastfoodfinder.data.local.db.store.model

import androidx.room.Entity
import androidx.room.PrimaryKey
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Store
import com.iceteaviet.fastfoodfinder.domain.model.Store

@Entity(tableName = "stores")
data class StoreEntity(
Expand All @@ -16,7 +16,7 @@ data class StoreEntity(
)

fun StoreEntity.toDomain(): Store {
return Store(id, title, address, latitude.toString(), longitude.toString(), telephone, type)
return Store(id, title, address, latitude, longitude, telephone, type)
}

fun Store.toEntity(): StoreEntity {
Expand All @@ -25,8 +25,8 @@ fun Store.toEntity(): StoreEntity {
type = type,
title = title,
address = address,
latitude = lat.toDoubleOrNull() ?: 0.0,
longitude = lng.toDoubleOrNull() ?: 0.0,
latitude = lat,
longitude = lng,
telephone = tel,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ class AppPreferencesHelper(private val preferences: PreferencesWrapper) : Prefer
preferences.putBoolean(KEY_LANGUAGE, isVietnamese)
}

override fun getLastKnownLocation(): Pair<Double, Double>? {
val latStr = preferences.getString(KEY_LAST_KNOWN_LAT, "")
val lngStr = preferences.getString(KEY_LAST_KNOWN_LNG, "")
if (latStr.isEmpty() || lngStr.isEmpty()) return null
return try {
Pair(latStr.toDouble(), lngStr.toDouble())
} catch (e: NumberFormatException) {
null
}
}

override fun setLastKnownLocation(lat: Double, lng: Double) {
preferences.putString(KEY_LAST_KNOWN_LAT, lat.toString())
preferences.putString(KEY_LAST_KNOWN_LNG, lng.toString())
}

companion object {
@VisibleForTesting
const val KEY_APP_LAUNCH_FIRST_TIME = "app_launch_first_time"
Expand All @@ -47,5 +63,11 @@ class AppPreferencesHelper(private val preferences: PreferencesWrapper) : Prefer

@VisibleForTesting
const val KEY_LANGUAGE = "lang"

@VisibleForTesting
const val KEY_LAST_KNOWN_LAT = "last_known_lat"

@VisibleForTesting
const val KEY_LAST_KNOWN_LNG = "last_known_lng"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ interface PreferencesHelper {
fun getIfLanguageIsVietnamese(): Boolean

fun setIfLanguageIsVietnamese(isVietnamese: Boolean)

fun getLastKnownLocation(): Pair<Double, Double>?

fun setLastKnownLocation(lat: Double, lng: Double)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.iceteaviet.fastfoodfinder.data.remote.routing

import com.iceteaviet.fastfoodfinder.data.remote.ApiEndPoint
import com.iceteaviet.fastfoodfinder.data.remote.routing.model.MapsDirection
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Store
import com.iceteaviet.fastfoodfinder.domain.model.Store
import com.iceteaviet.fastfoodfinder.utils.exception.NotFoundException
import com.iceteaviet.fastfoodfinder.utils.get
import kotlinx.coroutines.Dispatchers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.iceteaviet.fastfoodfinder.data.remote.routing

import com.iceteaviet.fastfoodfinder.data.remote.routing.model.MapsDirection
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Store
import com.iceteaviet.fastfoodfinder.domain.model.Store

interface MapsRoutingApiHelper {
suspend fun getMapsDirection(queries: Map<String, String>, store: Store): MapsDirection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import com.google.firebase.database.DatabaseError
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.ValueEventListener
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Comment
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Store
import com.iceteaviet.fastfoodfinder.data.remote.store.model.StoreDto
import com.iceteaviet.fastfoodfinder.domain.model.Store
import com.iceteaviet.fastfoodfinder.utils.getStoreType
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
Expand All @@ -14,45 +15,50 @@ import kotlin.coroutines.resumeWithException
class FirebaseStoreApiHelper(private val databaseRef: DatabaseReference) : StoreApiHelper {

override suspend fun getAllStores(): List<Store> = suspendCancellableCoroutine { cont ->
databaseRef.child(CHILD_STORES_LOCATION).addListenerForSingleValueEvent(object : ValueEventListener {
val ref = databaseRef.child(CHILD_STORES_LOCATION)
val listener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
cont.resume(parseStoresDataFromFirebase(dataSnapshot))
}

override fun onCancelled(databaseError: DatabaseError) {
cont.resumeWithException(databaseError.toException())
}
})
}
ref.addListenerForSingleValueEvent(listener)
cont.invokeOnCancellation { ref.removeEventListener(listener) }
}

override suspend fun getComments(storeId: String): List<Comment> = suspendCancellableCoroutine { cont ->
databaseRef.child(CHILD_COMMENT_LIST).child(storeId).addListenerForSingleValueEvent(object : ValueEventListener {
val ref = databaseRef.child(CHILD_COMMENT_LIST).child(storeId)
val listener = object : ValueEventListener {
override fun onCancelled(databaseError: DatabaseError) {
cont.resumeWithException(databaseError.toException())
}

override fun onDataChange(snapshot: DataSnapshot) {
cont.resume(parseCommentsDataFromFirebase(snapshot))
}
})
}
ref.addListenerForSingleValueEvent(listener)
cont.invokeOnCancellation { ref.removeEventListener(listener) }
}

override fun insertOrUpdateComment(storeId: String, comment: Comment) {
databaseRef.child(CHILD_COMMENT_LIST).child(storeId).push().setValue(comment)
}

private fun parseStoresDataFromFirebase(dataSnapshot: DataSnapshot): MutableList<Store> {
private fun parseStoresDataFromFirebase(dataSnapshot: DataSnapshot): List<Store> {
val storeList = ArrayList<Store>()
for (child in dataSnapshot.children) {
for (storeLocation in child.child(CHILD_MARKERS_ADD).children) {
val store = storeLocation.getValue(Store::class.java)
if (store != null) {
store.type = getStoreType(child.key)
storeList.add(store)
val dto = storeLocation.getValue(StoreDto::class.java)
if (dto != null) {
dto.type = getStoreType(child.key)
storeList.add(dto.toDomain())
}
}
}

return storeList
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.iceteaviet.fastfoodfinder.data.remote.store

import com.iceteaviet.fastfoodfinder.data.remote.store.model.Comment
import com.iceteaviet.fastfoodfinder.data.remote.store.model.Store
import com.iceteaviet.fastfoodfinder.domain.model.Store

interface StoreApiHelper {
suspend fun getAllStores(): List<Store>
Expand Down
Loading
Loading