diff --git a/app/src/androidTest/java/com/example/util/simpletimetracker/ChangeCategoryTest.kt b/app/src/androidTest/java/com/example/util/simpletimetracker/ChangeCategoryTest.kt index 568bd7b10..2d9dc0060 100644 --- a/app/src/androidTest/java/com/example/util/simpletimetracker/ChangeCategoryTest.kt +++ b/app/src/androidTest/java/com/example/util/simpletimetracker/ChangeCategoryTest.kt @@ -17,7 +17,6 @@ import com.example.util.simpletimetracker.utils.clickOnRecyclerItem import com.example.util.simpletimetracker.utils.clickOnViewWithText import com.example.util.simpletimetracker.utils.longClickOnView import com.example.util.simpletimetracker.utils.scrollRecyclerToView -import com.example.util.simpletimetracker.utils.tryAction import com.example.util.simpletimetracker.utils.typeTextIntoView import com.example.util.simpletimetracker.utils.withCardColor import dagger.hilt.android.testing.HiltAndroidTest diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/database/RecordTypeGoalDao.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/database/RecordTypeGoalDao.kt index ea85f8741..b69bf04af 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/database/RecordTypeGoalDao.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/database/RecordTypeGoalDao.kt @@ -27,10 +27,6 @@ interface RecordTypeGoalDao { @Query("SELECT * FROM recordTypeGoals WHERE category_id = :categoryId") suspend fun getByCategory(categoryId: Long): List - @Transaction - @Query("SELECT * FROM recordTypeGoals WHERE category_id IN (:categoryIds)") - suspend fun getByCategories(categoryIds: List): List - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(recordTypeGoal: RecordTypeGoalDBO): Long diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/extension/PrefsExtensions.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/extension/PrefsExtensions.kt deleted file mode 100644 index fd25e599e..000000000 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/extension/PrefsExtensions.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.util.simpletimetracker.data_local.extension - -import android.content.SharedPreferences -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -@Suppress("UNCHECKED_CAST") -inline fun SharedPreferences.delegate( - key: String, - default: T -) = object : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): T = - when (default) { - is Boolean -> (getBoolean(key, default) as? T) ?: default - is Int -> (getInt(key, default) as? T) ?: default - is Long -> (getLong(key, default) as? T) ?: default - is String -> (getString(key, default) as? T) ?: default - is Set<*> -> (getStringSet(key, default as? Set)?.toSet() as? T) ?: default - else -> throw IllegalArgumentException( - "Prefs delegate not implemented for class ${(default as Any?)?.javaClass}" - ) - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = with(edit()) { - when (value) { - is Boolean -> putBoolean(key, value) - is Int -> putInt(key, value) - is Long -> putLong(key, value) - is String -> putString(key, value) - is Set<*> -> putStringSet(key, value as? Set) - else -> throw IllegalArgumentException( - "Prefs delegate not implemented for class ${(default as Any?)?.javaClass}" - ) - } - apply() - } -} \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/ActivityFilterRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/ActivityFilterRepoImpl.kt index 9e1f1b7ef..7d1bc82e6 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/ActivityFilterRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/ActivityFilterRepoImpl.kt @@ -1,12 +1,12 @@ package com.example.util.simpletimetracker.data_local.repo import com.example.util.simpletimetracker.data_local.database.ActivityFilterDao +import com.example.util.simpletimetracker.data_local.utils.withLockedCache import com.example.util.simpletimetracker.data_local.mapper.ActivityFilterDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.removeIf import com.example.util.simpletimetracker.domain.model.ActivityFilter import com.example.util.simpletimetracker.domain.repo.ActivityFilterRepo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -16,37 +16,43 @@ class ActivityFilterRepoImpl @Inject constructor( private val activityFilterDataLocalMapper: ActivityFilterDataLocalMapper, ) : ActivityFilterRepo { - override suspend fun getAll(): List = withContext(Dispatchers.IO) { - Timber.d("getAll") - activityFilterDao.getAll() - .map(activityFilterDataLocalMapper::map) - } - - override suspend fun get(id: Long): ActivityFilter? = withContext(Dispatchers.IO) { - Timber.d("get") - activityFilterDao.get(id) - ?.let(activityFilterDataLocalMapper::map) - } - - override suspend fun add(activityFilter: ActivityFilter): Long = withContext(Dispatchers.IO) { - Timber.d("add") - return@withContext activityFilterDao.insert( - activityFilter.let(activityFilterDataLocalMapper::map) - ) - } - - override suspend fun changeSelected(id: Long, selected: Boolean) = withContext(Dispatchers.IO) { - Timber.d("changeSelected") - activityFilterDao.changeSelected(id, if (selected) 1 else 0) - } - - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { - Timber.d("remove") - activityFilterDao.delete(id) - } - - override suspend fun clear() = withContext(Dispatchers.IO) { - Timber.d("clear") - activityFilterDao.clear() - } + private var cache: List? = null + private val mutex: Mutex = Mutex() + + override suspend fun getAll(): List = mutex.withLockedCache( + logMessage = "getAll", + accessCache = { cache }, + accessSource = { activityFilterDao.getAll().map(activityFilterDataLocalMapper::map) }, + afterSourceAccess = { cache = it }, + ) + + override suspend fun get(id: Long): ActivityFilter? = mutex.withLockedCache( + logMessage = "get", + accessCache = { cache?.firstOrNull { it.id == id } }, + accessSource = { activityFilterDao.get(id)?.let(activityFilterDataLocalMapper::map) }, + ) + + override suspend fun add(activityFilter: ActivityFilter): Long = mutex.withLockedCache( + logMessage = "add", + accessSource = { activityFilterDao.insert(activityFilter.let(activityFilterDataLocalMapper::map)) }, + afterSourceAccess = { cache = null }, + ) + + override suspend fun changeSelected(id: Long, selected: Boolean) = mutex.withLockedCache( + logMessage = "changeSelected", + accessSource = { activityFilterDao.changeSelected(id, if (selected) 1 else 0) }, + afterSourceAccess = { cache = cache?.map { if (it.id == id) it.copy(selected = selected) else it } }, + ) + + override suspend fun remove(id: Long) = mutex.withLockedCache( + logMessage = "remove", + accessSource = { activityFilterDao.delete(id) }, + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, + ) + + override suspend fun clear() = mutex.withLockedCache( + logMessage = "clear", + accessSource = { activityFilterDao.clear() }, + afterSourceAccess = { cache = null }, + ) } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/CategoryRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/CategoryRepoImpl.kt index 174cca8f1..e22e30eb9 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/CategoryRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/CategoryRepoImpl.kt @@ -1,46 +1,52 @@ package com.example.util.simpletimetracker.data_local.repo import com.example.util.simpletimetracker.data_local.database.CategoryDao +import com.example.util.simpletimetracker.data_local.utils.withLockedCache import com.example.util.simpletimetracker.data_local.mapper.CategoryDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.removeIf import com.example.util.simpletimetracker.domain.model.Category import com.example.util.simpletimetracker.domain.repo.CategoryRepo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @Singleton class CategoryRepoImpl @Inject constructor( private val categoryDao: CategoryDao, - private val categoryDataLocalMapper: CategoryDataLocalMapper + private val categoryDataLocalMapper: CategoryDataLocalMapper, ) : CategoryRepo { - override suspend fun getAll(): List = withContext(Dispatchers.IO) { - Timber.d("getAll") - categoryDao.getAll() - .map(categoryDataLocalMapper::map) - } - - override suspend fun get(id: Long): Category? = withContext(Dispatchers.IO) { - Timber.d("get id") - categoryDao.get(id)?.let(categoryDataLocalMapper::map) - } - - override suspend fun add(category: Category): Long = withContext(Dispatchers.IO) { - Timber.d("add") - return@withContext categoryDao.insert( - category.let(categoryDataLocalMapper::map) - ) - } - - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { - Timber.d("remove") - categoryDao.delete(id) - } - - override suspend fun clear() = withContext(Dispatchers.IO) { - Timber.d("clear") - categoryDao.clear() - } + private var cache: List? = null + private val mutex: Mutex = Mutex() + + override suspend fun getAll(): List = mutex.withLockedCache( + logMessage = "getAll", + accessCache = { cache }, + accessSource = { categoryDao.getAll().map(categoryDataLocalMapper::map) }, + afterSourceAccess = { cache = it }, + ) + + override suspend fun get(id: Long): Category? = mutex.withLockedCache( + logMessage = "get id", + accessCache = { cache?.firstOrNull { it.id == id } }, + accessSource = { categoryDao.get(id)?.let(categoryDataLocalMapper::map) }, + ) + + override suspend fun add(category: Category): Long = mutex.withLockedCache( + logMessage = "add", + accessSource = { categoryDao.insert(category.let(categoryDataLocalMapper::map)) }, + afterSourceAccess = { cache = null }, + ) + + override suspend fun remove(id: Long) = mutex.withLockedCache( + logMessage = "remove", + accessSource = { categoryDao.delete(id) }, + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, + ) + + override suspend fun clear() = mutex.withLockedCache( + logMessage = "clear", + accessSource = { categoryDao.clear() }, + afterSourceAccess = { cache = null }, + ) } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/FavouriteCommentRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/FavouriteCommentRepoImpl.kt index 32ec5cf74..1c33d5dc3 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/FavouriteCommentRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/FavouriteCommentRepoImpl.kt @@ -2,13 +2,13 @@ package com.example.util.simpletimetracker.data_local.repo import com.example.util.simpletimetracker.data_local.database.FavouriteCommentDao import com.example.util.simpletimetracker.data_local.mapper.FavouriteCommentDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.logDataAccess import com.example.util.simpletimetracker.domain.model.FavouriteComment import com.example.util.simpletimetracker.domain.repo.FavouriteCommentRepo -import javax.inject.Inject -import javax.inject.Singleton import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton @Singleton class FavouriteCommentRepoImpl @Inject constructor( @@ -17,34 +17,34 @@ class FavouriteCommentRepoImpl @Inject constructor( ) : FavouriteCommentRepo { override suspend fun getAll(): List = withContext(Dispatchers.IO) { - Timber.d("getAll") + logDataAccess("getAll") favouriteCommentDao.getAll().map(FavouriteCommentDataLocalMapper::map) } override suspend fun get(id: Long): FavouriteComment? = withContext(Dispatchers.IO) { - Timber.d("get id") + logDataAccess("get id") favouriteCommentDao.get(id)?.let(FavouriteCommentDataLocalMapper::map) } override suspend fun get(text: String): FavouriteComment? = withContext(Dispatchers.IO) { - Timber.d("get text") + logDataAccess("get text") favouriteCommentDao.get(text)?.let(FavouriteCommentDataLocalMapper::map) } override suspend fun add(comment: FavouriteComment): Long = withContext(Dispatchers.IO) { - Timber.d("add") + logDataAccess("add") return@withContext favouriteCommentDao.insert( comment.let(FavouriteCommentDataLocalMapper::map) ) } override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { - Timber.d("remove") + logDataAccess("remove") favouriteCommentDao.delete(id) } override suspend fun clear() = withContext(Dispatchers.IO) { - Timber.d("clear") + logDataAccess("clear") favouriteCommentDao.clear() } } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/PrefsRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/PrefsRepoImpl.kt index fd3df8e90..1e2866ac6 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/PrefsRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/PrefsRepoImpl.kt @@ -1,7 +1,7 @@ package com.example.util.simpletimetracker.data_local.repo import android.content.SharedPreferences -import com.example.util.simpletimetracker.data_local.extension.delegate +import com.example.util.simpletimetracker.data_local.utils.delegate import com.example.util.simpletimetracker.domain.extension.orZero import com.example.util.simpletimetracker.domain.model.ChartFilterType import com.example.util.simpletimetracker.domain.model.QuickSettingsWidgetType diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordRepoImpl.kt index fbcc961d9..12f8e896b 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordRepoImpl.kt @@ -1,13 +1,16 @@ package com.example.util.simpletimetracker.data_local.repo +import androidx.collection.LruCache import com.example.util.simpletimetracker.data_local.database.RecordDao import com.example.util.simpletimetracker.data_local.mapper.RecordDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.logDataAccess +import com.example.util.simpletimetracker.data_local.utils.withLockedCache import com.example.util.simpletimetracker.domain.model.Range import com.example.util.simpletimetracker.domain.model.Record import com.example.util.simpletimetracker.domain.repo.RecordRepo import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -17,110 +20,144 @@ class RecordRepoImpl @Inject constructor( private val recordDataLocalMapper: RecordDataLocalMapper, ) : RecordRepo { - override suspend fun isEmpty(): Boolean = withContext(Dispatchers.IO) { - Timber.d("isEmpty") - return@withContext recordDao.isEmpty() == 0L - } + private var getFromRangeCache = LruCache>(10) + private var getFromRangeByTypeCache = LruCache>(1) + private var isEmpty: Boolean? = null + private val mutex: Mutex = Mutex() + + override suspend fun isEmpty(): Boolean = mutex.withLockedCache( + logMessage = "isEmpty", + accessCache = { isEmpty }, + accessSource = { recordDao.isEmpty() == 0L }, + afterSourceAccess = { isEmpty = it }, + ) override suspend fun getAll(): List = withContext(Dispatchers.IO) { - Timber.d("getAll") - recordDao.getAll() - .map(recordDataLocalMapper::map) + logDataAccess("getAll") + recordDao.getAll().map(recordDataLocalMapper::map) } override suspend fun getByType(typeIds: List): List = withContext(Dispatchers.IO) { - Timber.d("getByType") - recordDao.getByType(typeIds) - .map(recordDataLocalMapper::map) + logDataAccess("getByType") + recordDao.getByType(typeIds).map(recordDataLocalMapper::map) } override suspend fun getByTypeWithAnyComment(typeIds: List): List = withContext(Dispatchers.IO) { - Timber.d("getByTypeWithAnyComment") - recordDao.getByTypeWithAnyComment(typeIds) - .map(recordDataLocalMapper::map) + logDataAccess("getByTypeWithAnyComment") + recordDao.getByTypeWithAnyComment(typeIds).map(recordDataLocalMapper::map) } override suspend fun searchComment( text: String, ): List = withContext(Dispatchers.IO) { - Timber.d("searchComment") - recordDao.searchComment(text) - .map(recordDataLocalMapper::map) + logDataAccess("searchComment") + recordDao.searchComment(text).map(recordDataLocalMapper::map) } override suspend fun searchByTypeWithComment( typeIds: List, text: String, ): List = withContext(Dispatchers.IO) { - Timber.d("searchByTypeWithComment") - recordDao.searchByTypeWithComment(typeIds, text) - .map(recordDataLocalMapper::map) + logDataAccess("searchByTypeWithComment") + recordDao.searchByTypeWithComment(typeIds, text).map(recordDataLocalMapper::map) } override suspend fun searchAnyComments(): List = withContext(Dispatchers.IO) { - Timber.d("searchAnyComments") - recordDao.searchAnyComments() - .map(recordDataLocalMapper::map) + logDataAccess("searchAnyComments") + recordDao.searchAnyComments().map(recordDataLocalMapper::map) } override suspend fun get(id: Long): Record? = withContext(Dispatchers.IO) { - Timber.d("get") - recordDao.get(id) - ?.let(recordDataLocalMapper::map) + logDataAccess("get") + recordDao.get(id)?.let(recordDataLocalMapper::map) } - override suspend fun getFromRange(range: Range): List = - withContext(Dispatchers.IO) { - Timber.d("getFromRange") - recordDao.getFromRange(range.timeStarted, range.timeEnded) - .map(recordDataLocalMapper::map) - } + override suspend fun getFromRange(range: Range): List { + val cacheKey = GetFromRangeKey(range) + return mutex.withLockedCache( + logMessage = "getFromRange", + accessCache = { getFromRangeCache.get(cacheKey) }, + accessSource = { + recordDao.getFromRange(range.timeStarted, range.timeEnded) + .map(recordDataLocalMapper::map) + }, + afterSourceAccess = { getFromRangeCache.put(cacheKey, it) }, + ) + } - override suspend fun getFromRangeByType(typeIds: List, range: Range): List = - withContext(Dispatchers.IO) { - Timber.d("getFromRangeByType") - recordDao.getFromRangeByType(typeIds, range.timeStarted, range.timeEnded) - .map(recordDataLocalMapper::map) - } + override suspend fun getFromRangeByType(typeIds: List, range: Range): List { + val cacheKey = GetFromRangeByTypeKey(typeIds, range) + return mutex.withLockedCache( + logMessage = "getFromRangeByType", + accessCache = { getFromRangeByTypeCache.get(cacheKey) }, + accessSource = { + recordDao.getFromRangeByType( + typesIds = typeIds, + start = range.timeStarted, + end = range.timeEnded, + ).map(recordDataLocalMapper::map) + }, + afterSourceAccess = { getFromRangeByTypeCache.put(cacheKey, it) }, + ) + } override suspend fun getPrev(timeStarted: Long, limit: Long): List = withContext(Dispatchers.IO) { - Timber.d("getPrev") + logDataAccess("getPrev") recordDao.getPrev(timeStarted, limit) .map(recordDataLocalMapper::map) } override suspend fun getNext(timeEnded: Long): Record? = withContext(Dispatchers.IO) { - Timber.d("getNext") + logDataAccess("getNext") recordDao.getNext(timeEnded) ?.let(recordDataLocalMapper::map) } - override suspend fun add(record: Record): Long = withContext(Dispatchers.IO) { - Timber.d("add") - // Drop milliseconds. - val adjustedRecord = record.copy( - timeStarted = record.timeStarted / 1000 * 1000, - timeEnded = record.timeEnded / 1000 * 1000, - ) - return@withContext recordDao.insert( - adjustedRecord.let(recordDataLocalMapper::map) - ) + override suspend fun add(record: Record): Long = mutex.withLockedCache( + logMessage = "add", + accessSource = { + // Drop milliseconds. + val adjustedRecord = record.copy( + timeStarted = record.timeStarted / 1000 * 1000, + timeEnded = record.timeEnded / 1000 * 1000, + ) + recordDao.insert(adjustedRecord.let(recordDataLocalMapper::map)) + }, + afterSourceAccess = { clearCache() }, + ) + + override suspend fun remove(id: Long) = mutex.withLockedCache( + logMessage = "remove", + accessSource = { recordDao.delete(id) }, + afterSourceAccess = { clearCache() }, + ) + + override suspend fun removeByType(typeId: Long) = mutex.withLockedCache( + logMessage = "removeByType", + accessSource = { recordDao.deleteByType(typeId) }, + afterSourceAccess = { clearCache() }, + ) + + override suspend fun clear() = mutex.withLockedCache( + logMessage = "clear", + accessSource = { recordDao.clear() }, + afterSourceAccess = { clearCache() }, + ) + + private fun clearCache() { + getFromRangeCache.evictAll() + getFromRangeByTypeCache.evictAll() + isEmpty = null } - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { - Timber.d("remove") - recordDao.delete(id) - } - - override suspend fun removeByType(typeId: Long) = withContext(Dispatchers.IO) { - Timber.d("removeByType") - recordDao.deleteByType(typeId) - } + private data class GetFromRangeByTypeKey( + val typeIds: List, + val range: Range, + ) - override suspend fun clear() = withContext(Dispatchers.IO) { - Timber.d("clear") - recordDao.clear() - } + private data class GetFromRangeKey( + val range: Range, + ) } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTagRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTagRepoImpl.kt index b95dc0019..8792c37d2 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTagRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTagRepoImpl.kt @@ -1,78 +1,94 @@ package com.example.util.simpletimetracker.data_local.repo import com.example.util.simpletimetracker.data_local.database.RecordTagDao +import com.example.util.simpletimetracker.data_local.utils.withLockedCache import com.example.util.simpletimetracker.data_local.mapper.RecordTagDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.removeIf import com.example.util.simpletimetracker.domain.model.RecordTag import com.example.util.simpletimetracker.domain.repo.RecordTagRepo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @Singleton class RecordTagRepoImpl @Inject constructor( private val dao: RecordTagDao, - private val mapper: RecordTagDataLocalMapper + private val mapper: RecordTagDataLocalMapper, ) : RecordTagRepo { - override suspend fun isEmpty(): Boolean = withContext(Dispatchers.IO) { - Timber.d("isEmpty") - return@withContext dao.isEmpty() == 0L - } + private var cache: List? = null + private val mutex: Mutex = Mutex() - override suspend fun getAll(): List = withContext(Dispatchers.IO) { - Timber.d("getAll") - dao.getAll().map(mapper::map) - } + override suspend fun isEmpty(): Boolean = mutex.withLockedCache( + logMessage = "isEmpty", + accessCache = { cache?.isEmpty() }, + accessSource = { dao.isEmpty() == 0L }, + ) - override suspend fun get(id: Long): RecordTag? = withContext(Dispatchers.IO) { - Timber.d("get") - dao.get(id)?.let(mapper::map) - } + override suspend fun getAll(): List = mutex.withLockedCache( + logMessage = "getAll", + accessCache = { cache }, + accessSource = { dao.getAll().map(mapper::map) }, + afterSourceAccess = { cache = it }, + ) - override suspend fun getByType(typeId: Long): List = withContext(Dispatchers.IO) { - Timber.d("getByType") - dao.getByType(typeId).map(mapper::map) - } + override suspend fun get(id: Long): RecordTag? = mutex.withLockedCache( + logMessage = "get", + accessCache = { cache?.firstOrNull { it.id == id } }, + accessSource = { dao.get(id)?.let(mapper::map) }, + ) - override suspend fun getUntyped(): List = withContext(Dispatchers.IO) { - Timber.d("getUntyped") - dao.getUntyped().map(mapper::map) - } + override suspend fun getByType(typeId: Long): List = mutex.withLockedCache( + logMessage = "getByType", + accessCache = { cache?.filter { it.typeId == typeId } }, + accessSource = { dao.getByType(typeId).map(mapper::map) }, + ) - override suspend fun getByTypeOrUntyped(typeId: Long): List = withContext(Dispatchers.IO) { - Timber.d("getByTypeOrUntyped") - dao.getByTypeOrUntyped(typeId).map(mapper::map) - } + override suspend fun getUntyped(): List = mutex.withLockedCache( + logMessage = "getUntyped", + accessCache = { cache?.filter { it.typeId == 0L } }, + accessSource = { dao.getUntyped().map(mapper::map) }, + ) - override suspend fun add(tag: RecordTag): Long = withContext(Dispatchers.IO) { - Timber.d("add") - dao.insert(tag.let(mapper::map)) - } + override suspend fun getByTypeOrUntyped(typeId: Long): List = mutex.withLockedCache( + logMessage = "getByTypeOrUntyped", + accessCache = { cache?.filter { it.typeId == 0L || it.typeId == typeId } }, + accessSource = { dao.getByTypeOrUntyped(typeId).map(mapper::map) }, + ) - override suspend fun archive(id: Long) = withContext(Dispatchers.IO) { - Timber.d("archive") - dao.archive(id) - } + override suspend fun add(tag: RecordTag): Long = mutex.withLockedCache( + logMessage = "add", + accessSource = { dao.insert(tag.let(mapper::map)) }, + afterSourceAccess = { cache = null }, + ) - override suspend fun restore(id: Long) = withContext(Dispatchers.IO) { - Timber.d("restore") - dao.restore(id) - } + override suspend fun archive(id: Long) = mutex.withLockedCache( + logMessage = "archive", + accessSource = { dao.archive(id) }, + afterSourceAccess = { cache = cache?.map { if (it.id == id) it.copy(archived = true) else it } }, + ) - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { - Timber.d("remove") - dao.delete(id) - } + override suspend fun restore(id: Long) = mutex.withLockedCache( + logMessage = "restore", + accessSource = { dao.restore(id) }, + afterSourceAccess = { cache = cache?.map { if (it.id == id) it.copy(archived = false) else it } }, + ) - override suspend fun removeByType(typeId: Long) = withContext(Dispatchers.IO) { - Timber.d("removeByType") - dao.deleteByType(typeId) - } + override suspend fun remove(id: Long) = mutex.withLockedCache( + logMessage = "remove", + accessSource = { dao.delete(id) }, + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, + ) - override suspend fun clear() = withContext(Dispatchers.IO) { - Timber.d("clear") - dao.clear() - } + override suspend fun removeByType(typeId: Long) = mutex.withLockedCache( + logMessage = "removeByType", + accessSource = { dao.deleteByType(typeId) }, + afterSourceAccess = { cache = cache?.removeIf { it.typeId == typeId } }, + ) + + override suspend fun clear() = mutex.withLockedCache( + logMessage = "clear", + accessSource = { dao.clear() }, + afterSourceAccess = { cache = null }, + ) } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordToRecordTagRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordToRecordTagRepoImpl.kt index 0e4ebeb25..c07b13940 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordToRecordTagRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordToRecordTagRepoImpl.kt @@ -2,11 +2,11 @@ package com.example.util.simpletimetracker.data_local.repo import com.example.util.simpletimetracker.data_local.database.RecordToRecordTagDao import com.example.util.simpletimetracker.data_local.mapper.RecordToRecordTagDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.logDataAccess import com.example.util.simpletimetracker.domain.model.RecordToRecordTag import com.example.util.simpletimetracker.domain.repo.RecordToRecordTagRepo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -18,25 +18,25 @@ class RecordToRecordTagRepoImpl @Inject constructor( override suspend fun getAll(): List = withContext(Dispatchers.IO) { - Timber.d("get all") + logDataAccess("get all") dao.getAll().map(mapper::map) } override suspend fun getTagIdsByRecordId(recordId: Long): List = withContext(Dispatchers.IO) { - Timber.d("get tag ids") + logDataAccess("get tag ids") dao.getTagIdsByRecordId(recordId) } override suspend fun getRecordIdsByTagId(tagId: Long): List = withContext(Dispatchers.IO) { - Timber.d("get record ids") + logDataAccess("get record ids") dao.getRecordIdsByTagId(tagId) } override suspend fun add(recordToRecordTag: RecordToRecordTag) = withContext(Dispatchers.IO) { - Timber.d("add") + logDataAccess("add") recordToRecordTag .let(mapper::map) .let { @@ -46,7 +46,7 @@ class RecordToRecordTagRepoImpl @Inject constructor( override suspend fun addRecordTags(recordId: Long, tagIds: List) = withContext(Dispatchers.IO) { - Timber.d("add record tags") + logDataAccess("add record tags") tagIds.map { mapper.map(recordId = recordId, recordTagId = it) }.let { @@ -56,7 +56,7 @@ class RecordToRecordTagRepoImpl @Inject constructor( override suspend fun removeRecordTags(recordId: Long, tagIds: List) = withContext(Dispatchers.IO) { - Timber.d("remove record tags") + logDataAccess("remove record tags") tagIds.map { mapper.map(recordId = recordId, recordTagId = it) }.let { @@ -66,19 +66,19 @@ class RecordToRecordTagRepoImpl @Inject constructor( override suspend fun removeAllByTagId(tagId: Long) = withContext(Dispatchers.IO) { - Timber.d("remove all by tagId") + logDataAccess("remove all by tagId") dao.deleteAllByTagId(tagId) } override suspend fun removeAllByRecordId(recordId: Long) = withContext(Dispatchers.IO) { - Timber.d("remove all by recordId") + logDataAccess("remove all by recordId") dao.deleteAllByRecordId(recordId) } override suspend fun clear() = withContext(Dispatchers.IO) { - Timber.d("clear") + logDataAccess("clear") dao.clear() } } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeCategoryRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeCategoryRepoImpl.kt index c342d1953..3e3e12b97 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeCategoryRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeCategoryRepoImpl.kt @@ -1,14 +1,14 @@ package com.example.util.simpletimetracker.data_local.repo import com.example.util.simpletimetracker.data_local.database.RecordTypeCategoryDao +import com.example.util.simpletimetracker.data_local.utils.withLockedCache import com.example.util.simpletimetracker.data_local.mapper.CategoryDataLocalMapper import com.example.util.simpletimetracker.data_local.mapper.RecordTypeCategoryDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.removeIf import com.example.util.simpletimetracker.domain.model.Category import com.example.util.simpletimetracker.domain.model.RecordTypeCategory import com.example.util.simpletimetracker.domain.repo.RecordTypeCategoryRepo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -16,102 +16,106 @@ import javax.inject.Singleton class RecordTypeCategoryRepoImpl @Inject constructor( private val recordTypeCategoryDao: RecordTypeCategoryDao, private val categoryDataLocalMapper: CategoryDataLocalMapper, - private val recordTypeCategoryDataLocalMapper: RecordTypeCategoryDataLocalMapper + private val recordTypeCategoryDataLocalMapper: RecordTypeCategoryDataLocalMapper, ) : RecordTypeCategoryRepo { - override suspend fun getAll(): List = - withContext(Dispatchers.IO) { - Timber.d("get all") - recordTypeCategoryDao.getAll() - .map(recordTypeCategoryDataLocalMapper::map) - } + private var cache: List? = null + private val mutex: Mutex = Mutex() - override suspend fun getCategoriesByType(typeId: Long): List = - withContext(Dispatchers.IO) { - Timber.d("get categories") + override suspend fun getAll(): List = mutex.withLockedCache( + logMessage = "get all", + accessCache = { cache }, + accessSource = { recordTypeCategoryDao.getAll().map(recordTypeCategoryDataLocalMapper::map) }, + afterSourceAccess = { cache = it }, + ) + + override suspend fun getCategoriesByType(typeId: Long): List = mutex.withLockedCache( + logMessage = "get categories", + accessSource = { recordTypeCategoryDao.getTypeWithCategories(typeId) ?.categories ?.map(categoryDataLocalMapper::map) .orEmpty() - } + }, + ) - override suspend fun getCategoryIdsByType(typeId: Long): List = - withContext(Dispatchers.IO) { - Timber.d("get category ids") - recordTypeCategoryDao.getCategoryIdsByType(typeId) - } + override suspend fun getCategoryIdsByType(typeId: Long): Set = mutex.withLockedCache( + logMessage = "get category ids", + accessCache = { cache?.filter { it.recordTypeId == typeId }?.map { it.categoryId }?.toSet() }, + accessSource = { recordTypeCategoryDao.getCategoryIdsByType(typeId).toSet() }, + ) - override suspend fun add(recordTypeCategory: RecordTypeCategory) = - withContext(Dispatchers.IO) { - Timber.d("add") + override suspend fun add(recordTypeCategory: RecordTypeCategory) = mutex.withLockedCache( + logMessage = "add", + accessSource = { recordTypeCategory .let(recordTypeCategoryDataLocalMapper::map) - .let { - recordTypeCategoryDao.insert(listOf(it)) - } - } + .let { recordTypeCategoryDao.insert(listOf(it)) } + }, + afterSourceAccess = { cache = null }, + ) - override suspend fun addCategories(typeId: Long, categoryIds: List) = - withContext(Dispatchers.IO) { - Timber.d("add categories") + override suspend fun addCategories(typeId: Long, categoryIds: List) = mutex.withLockedCache( + logMessage = "add categories", + accessSource = { categoryIds.map { recordTypeCategoryDataLocalMapper.map(typeId = typeId, categoryId = it) - }.let { - recordTypeCategoryDao.insert(it) - } - } + }.let { recordTypeCategoryDao.insert(it) } + }, + afterSourceAccess = { cache = null }, + ) - override suspend fun removeCategories(typeId: Long, categoryIds: List) = - withContext(Dispatchers.IO) { - Timber.d("remove categories") + override suspend fun removeCategories(typeId: Long, categoryIds: List) = mutex.withLockedCache( + logMessage = "remove categories", + accessSource = { categoryIds.map { recordTypeCategoryDataLocalMapper.map(typeId = typeId, categoryId = it) - }.let { - recordTypeCategoryDao.delete(it) - } - } + }.let { recordTypeCategoryDao.delete(it) } + }, + afterSourceAccess = { cache = null }, + ) - override suspend fun getTypeIdsByCategory(categoryId: Long): List = - withContext(Dispatchers.IO) { - Timber.d("get type ids") - recordTypeCategoryDao.getTypeIdsByCategory(categoryId) - } + override suspend fun getTypeIdsByCategory(categoryId: Long): Set = mutex.withLockedCache( + logMessage = "get type ids", + accessCache = { cache?.filter { it.categoryId == categoryId }?.map { it.recordTypeId }?.toSet() }, + accessSource = { recordTypeCategoryDao.getTypeIdsByCategory(categoryId).toSet() }, + ) - override suspend fun addTypes(categoryId: Long, typeIds: List) = - withContext(Dispatchers.IO) { - Timber.d("add types") + override suspend fun addTypes(categoryId: Long, typeIds: List) = mutex.withLockedCache( + logMessage = "add types", + accessSource = { typeIds.map { recordTypeCategoryDataLocalMapper.map(typeId = it, categoryId = categoryId) - }.let { - recordTypeCategoryDao.insert(it) - } - } + }.let { recordTypeCategoryDao.insert(it) } + }, + afterSourceAccess = { cache = null }, + ) - override suspend fun removeTypes(categoryId: Long, typeIds: List) = - withContext(Dispatchers.IO) { - Timber.d("remove types") + override suspend fun removeTypes(categoryId: Long, typeIds: List) = mutex.withLockedCache( + logMessage = "remove types", + accessSource = { typeIds.map { recordTypeCategoryDataLocalMapper.map(typeId = it, categoryId = categoryId) - }.let { - recordTypeCategoryDao.delete(it) - } - } + }.let { recordTypeCategoryDao.delete(it) } + }, + afterSourceAccess = { cache = null }, + ) - override suspend fun removeAll(categoryId: Long) = - withContext(Dispatchers.IO) { - Timber.d("removeAll") - recordTypeCategoryDao.deleteAll(categoryId) - } + override suspend fun removeAll(categoryId: Long) = mutex.withLockedCache( + logMessage = "removeAll", + accessSource = { recordTypeCategoryDao.deleteAll(categoryId) }, + afterSourceAccess = { cache = cache?.removeIf { it.categoryId == categoryId } }, + ) - override suspend fun removeAllByType(typeId: Long) = - withContext(Dispatchers.IO) { - Timber.d("removeAllByType") - recordTypeCategoryDao.deleteAllByType(typeId) - } + override suspend fun removeAllByType(typeId: Long) = mutex.withLockedCache( + logMessage = "removeAllByType", + accessSource = { recordTypeCategoryDao.deleteAllByType(typeId) }, + afterSourceAccess = { cache = cache?.removeIf { it.recordTypeId == typeId } }, + ) - override suspend fun clear() = - withContext(Dispatchers.IO) { - Timber.d("clear") - recordTypeCategoryDao.clear() - } + override suspend fun clear() = mutex.withLockedCache( + logMessage = "clear", + accessSource = { recordTypeCategoryDao.clear() }, + afterSourceAccess = { cache = null }, + ) } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeGoalRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeGoalRepoImpl.kt index 874f44aad..ccd50cd86 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeGoalRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeGoalRepoImpl.kt @@ -2,75 +2,96 @@ package com.example.util.simpletimetracker.data_local.repo import com.example.util.simpletimetracker.data_local.database.RecordTypeGoalDao import com.example.util.simpletimetracker.data_local.mapper.RecordTypeGoalDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.removeIf +import com.example.util.simpletimetracker.data_local.utils.withLockedCache import com.example.util.simpletimetracker.domain.model.RecordTypeGoal +import com.example.util.simpletimetracker.domain.model.RecordTypeGoal.IdData import com.example.util.simpletimetracker.domain.repo.RecordTypeGoalRepo import javax.inject.Inject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber +import kotlinx.coroutines.sync.Mutex class RecordTypeGoalRepoImpl @Inject constructor( private val dao: RecordTypeGoalDao, private val mapper: RecordTypeGoalDataLocalMapper, ) : RecordTypeGoalRepo { - override suspend fun getAll(): List = withContext(Dispatchers.IO) { - Timber.d("getAll") - dao.getAll().map(mapper::map) - } + private var cache: List? = null + private val mutex: Mutex = Mutex() - override suspend fun getAllTypeGoals(): List = withContext(Dispatchers.IO) { - Timber.d("getAllTypeGoals") - dao.getAllTypeGoals().map(mapper::map) - } + override suspend fun getAll(): List = mutex.withLockedCache( + logMessage = "getAll", + accessCache = { cache }, + accessSource = { dao.getAll().map(mapper::map) }, + afterSourceAccess = { cache = it }, + ) - override suspend fun getAllCategoryGoals(): List = withContext(Dispatchers.IO) { - Timber.d("getAllCategoryGoals") - dao.getAllCategoryGoals().map(mapper::map) - } + override suspend fun getAllTypeGoals(): List = mutex.withLockedCache( + logMessage = "getAllTypeGoals", + accessCache = { cache?.filter { it.isType() && it.idData.value != 0L } }, + accessSource = { dao.getAllTypeGoals().map(mapper::map) }, + afterSourceAccess = { initializeCache() }, + ) - override suspend fun getByType(typeId: Long): List = withContext(Dispatchers.IO) { - Timber.d("getByType") - dao.getByType(typeId) - .map(mapper::map) - } + override suspend fun getAllCategoryGoals(): List = mutex.withLockedCache( + logMessage = "getAllCategoryGoals", + accessCache = { cache?.filter { it.isCategory() && it.idData.value != 0L } }, + accessSource = { dao.getAllCategoryGoals().map(mapper::map) }, + afterSourceAccess = { initializeCache() }, + ) - override suspend fun getByCategory(categoryId: Long): List = withContext(Dispatchers.IO) { - Timber.d("getByCategory") - dao.getByCategory(categoryId) - .map(mapper::map) - } + override suspend fun getByType(typeId: Long): List = mutex.withLockedCache( + logMessage = "getByType", + accessCache = { cache?.filter { it.isType() && it.idData.value == typeId } }, + accessSource = { dao.getByType(typeId).map(mapper::map) }, + afterSourceAccess = { initializeCache() }, + ) - override suspend fun getByCategories(categoryIds: List): List = withContext(Dispatchers.IO) { - Timber.d("getByCategories") - dao.getByCategories(categoryIds) - .map(mapper::map) - } + override suspend fun getByCategory(categoryId: Long): List = mutex.withLockedCache( + logMessage = "getByCategory", + accessCache = { cache?.filter { it.isCategory() && it.idData.value == categoryId } }, + accessSource = { dao.getByCategory(categoryId).map(mapper::map) }, + afterSourceAccess = { initializeCache() }, + ) - override suspend fun add(recordTypeGoal: RecordTypeGoal): Long = withContext(Dispatchers.IO) { - Timber.d("add") - return@withContext dao.insert( - recordTypeGoal.let(mapper::map), - ) - } + override suspend fun add(recordTypeGoal: RecordTypeGoal): Long = mutex.withLockedCache( + logMessage = "add", + accessSource = { dao.insert(recordTypeGoal.let(mapper::map)) }, + afterSourceAccess = { cache = null }, + ) - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { - Timber.d("remove") - dao.delete(id) - } + override suspend fun remove(id: Long) = mutex.withLockedCache( + logMessage = "remove", + accessSource = { dao.delete(id) }, + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, + ) + + override suspend fun removeByType(typeId: Long) = mutex.withLockedCache( + logMessage = "removeByType", + accessSource = { dao.deleteByType(typeId) }, + afterSourceAccess = { cache = cache?.removeIf { it.isType() && it.idData.value == typeId } }, + ) + + override suspend fun removeByCategory(categoryId: Long) = mutex.withLockedCache( + logMessage = "removeByCategory", + accessSource = { dao.deleteByCategory(categoryId) }, + afterSourceAccess = { cache = cache?.removeIf { it.isCategory() && it.idData.value == categoryId } }, + ) + + override suspend fun clear() = mutex.withLockedCache( + logMessage = "clear", + accessSource = { dao.clear() }, + afterSourceAccess = { cache = null }, + ) - override suspend fun removeByType(typeId: Long) = withContext(Dispatchers.IO) { - Timber.d("removeByType") - dao.deleteByType(typeId) + private suspend fun initializeCache() { + cache = dao.getAll().map(mapper::map) } - override suspend fun removeByCategory(categoryId: Long) = withContext(Dispatchers.IO) { - Timber.d("removeByCategory") - dao.deleteByCategory(categoryId) + private fun RecordTypeGoal.isType(): Boolean { + return idData is IdData.Type } - override suspend fun clear() = withContext(Dispatchers.IO) { - Timber.d("clear") - dao.clear() + private fun RecordTypeGoal.isCategory(): Boolean { + return idData is IdData.Category } } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeRepoImpl.kt index aa6abf10a..cf2f8c86b 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeRepoImpl.kt @@ -1,60 +1,70 @@ package com.example.util.simpletimetracker.data_local.repo import com.example.util.simpletimetracker.data_local.database.RecordTypeDao +import com.example.util.simpletimetracker.data_local.utils.withLockedCache import com.example.util.simpletimetracker.data_local.mapper.RecordTypeDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.removeIf import com.example.util.simpletimetracker.domain.model.RecordType import com.example.util.simpletimetracker.domain.repo.RecordTypeRepo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @Singleton class RecordTypeRepoImpl @Inject constructor( private val recordTypeDao: RecordTypeDao, - private val recordTypeDataLocalMapper: RecordTypeDataLocalMapper + private val recordTypeDataLocalMapper: RecordTypeDataLocalMapper, ) : RecordTypeRepo { - override suspend fun getAll(): List = withContext(Dispatchers.IO) { - Timber.d("getAll") - recordTypeDao.getAll().map(recordTypeDataLocalMapper::map) - } - - override suspend fun get(id: Long): RecordType? = withContext(Dispatchers.IO) { - Timber.d("get id") - recordTypeDao.get(id)?.let(recordTypeDataLocalMapper::map) - } - - override suspend fun get(name: String): RecordType? = withContext(Dispatchers.IO) { - Timber.d("get name") - recordTypeDao.get(name)?.let(recordTypeDataLocalMapper::map) - } - - override suspend fun add(recordType: RecordType): Long = withContext(Dispatchers.IO) { - Timber.d("add") - return@withContext recordTypeDao.insert( - recordType.let(recordTypeDataLocalMapper::map) - ) - } - - override suspend fun archive(id: Long) = withContext(Dispatchers.IO) { - Timber.d("archive") - recordTypeDao.archive(id) - } - - override suspend fun restore(id: Long) = withContext(Dispatchers.IO) { - Timber.d("restore") - recordTypeDao.restore(id) - } - - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { - Timber.d("remove") - recordTypeDao.delete(id) - } - - override suspend fun clear() = withContext(Dispatchers.IO) { - Timber.d("clear") - recordTypeDao.clear() - } + private var cache: List? = null + private val mutex: Mutex = Mutex() + + override suspend fun getAll(): List = mutex.withLockedCache( + logMessage = "getAll", + accessCache = { cache }, + accessSource = { recordTypeDao.getAll().map(recordTypeDataLocalMapper::map) }, + afterSourceAccess = { cache = it }, + ) + + override suspend fun get(id: Long): RecordType? = mutex.withLockedCache( + logMessage = "get id", + accessCache = { cache?.firstOrNull { it.id == id } }, + accessSource = { recordTypeDao.get(id)?.let(recordTypeDataLocalMapper::map) }, + ) + + override suspend fun get(name: String): RecordType? = mutex.withLockedCache( + logMessage = "get name", + accessCache = { cache?.firstOrNull { it.name == name } }, + accessSource = { recordTypeDao.get(name)?.let(recordTypeDataLocalMapper::map) }, + ) + + override suspend fun add(recordType: RecordType): Long = mutex.withLockedCache( + logMessage = "add", + accessSource = { recordTypeDao.insert(recordType.let(recordTypeDataLocalMapper::map)) }, + afterSourceAccess = { cache = null }, + ) + + override suspend fun archive(id: Long) = mutex.withLockedCache( + logMessage = "archive", + accessSource = { recordTypeDao.archive(id) }, + afterSourceAccess = { cache = cache?.map { if (it.id == id) it.copy(hidden = true) else it } }, + ) + + override suspend fun restore(id: Long) = mutex.withLockedCache( + logMessage = "restore", + accessSource = { recordTypeDao.restore(id) }, + afterSourceAccess = { cache = cache?.map { if (it.id == id) it.copy(hidden = false) else it } }, + ) + + override suspend fun remove(id: Long) = mutex.withLockedCache( + logMessage = "remove", + accessSource = { recordTypeDao.delete(id) }, + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, + ) + + override suspend fun clear() = mutex.withLockedCache( + logMessage = "clear", + accessSource = { recordTypeDao.clear() }, + afterSourceAccess = { cache = null }, + ) } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RepoConstants.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RepoConstants.kt new file mode 100644 index 000000000..1dbee8f56 --- /dev/null +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RepoConstants.kt @@ -0,0 +1,6 @@ +package com.example.util.simpletimetracker.data_local.repo + +object RepoConstants { + const val LOG_MESSAGE_PREFIX = "DB_ACCESS" + const val LOG_MESSAGE_CACHE = "CACHE" +} \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RunningRecordRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RunningRecordRepoImpl.kt index aa802b428..ff46083c5 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RunningRecordRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RunningRecordRepoImpl.kt @@ -2,49 +2,57 @@ package com.example.util.simpletimetracker.data_local.repo import com.example.util.simpletimetracker.data_local.database.RunningRecordDao import com.example.util.simpletimetracker.data_local.mapper.RunningRecordDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.removeIf +import com.example.util.simpletimetracker.data_local.utils.withLockedCache import com.example.util.simpletimetracker.domain.model.RunningRecord import com.example.util.simpletimetracker.domain.repo.RunningRecordRepo -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber +import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @Singleton class RunningRecordRepoImpl @Inject constructor( private val runningRecordDao: RunningRecordDao, - private val runningRunningRecordLocalMapper: RunningRecordDataLocalMapper + private val runningRunningRecordLocalMapper: RunningRecordDataLocalMapper, ) : RunningRecordRepo { - override suspend fun isEmpty(): Boolean = withContext(Dispatchers.IO) { - Timber.d("isEmpty") - return@withContext runningRecordDao.isEmpty() == 0L - } - - override suspend fun getAll(): List = withContext(Dispatchers.IO) { - Timber.d("getAll") - runningRecordDao.getAll().map(runningRunningRecordLocalMapper::map) - } - - override suspend fun get(id: Long): RunningRecord? = withContext(Dispatchers.IO) { - Timber.d("get") - runningRecordDao.get(id)?.let(runningRunningRecordLocalMapper::map) - } - - override suspend fun add(runningRecord: RunningRecord): Long = withContext(Dispatchers.IO) { - Timber.d("add") - return@withContext runningRecordDao.insert( - runningRecord.let(runningRunningRecordLocalMapper::map) - ) - } - - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { - Timber.d("remove") - runningRecordDao.delete(id) - } - - override suspend fun clear() = withContext(Dispatchers.IO) { - Timber.d("clear") - runningRecordDao.clear() - } + private var cache: List? = null + private val mutex: Mutex = Mutex() + + override suspend fun isEmpty(): Boolean = mutex.withLockedCache( + logMessage = "isEmpty", + accessCache = { cache?.isEmpty() }, + accessSource = { runningRecordDao.isEmpty() == 0L }, + ) + + override suspend fun getAll(): List = mutex.withLockedCache( + logMessage = "getAll", + accessCache = { cache }, + accessSource = { runningRecordDao.getAll().map(runningRunningRecordLocalMapper::map) }, + afterSourceAccess = { cache = it }, + ) + + override suspend fun get(id: Long): RunningRecord? = mutex.withLockedCache( + logMessage = "get", + accessCache = { cache?.firstOrNull { it.id == id } }, + accessSource = { runningRecordDao.get(id)?.let(runningRunningRecordLocalMapper::map) }, + ) + + override suspend fun add(runningRecord: RunningRecord): Long = mutex.withLockedCache( + logMessage = "add", + accessSource = { runningRecordDao.insert(runningRecord.let(runningRunningRecordLocalMapper::map)) }, + afterSourceAccess = { cache = null }, + ) + + override suspend fun remove(id: Long) = mutex.withLockedCache( + logMessage = "remove", + accessSource = { runningRecordDao.delete(id) }, + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, + ) + + override suspend fun clear() = mutex.withLockedCache( + logMessage = "clear", + accessSource = { runningRecordDao.clear() }, + afterSourceAccess = { cache = null }, + ) } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RunningRecordToRecordTagRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RunningRecordToRecordTagRepoImpl.kt index 108cf2e42..d7339a8ac 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RunningRecordToRecordTagRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RunningRecordToRecordTagRepoImpl.kt @@ -2,10 +2,10 @@ package com.example.util.simpletimetracker.data_local.repo import com.example.util.simpletimetracker.data_local.database.RunningRecordToRecordTagDao import com.example.util.simpletimetracker.data_local.mapper.RunningRecordToRecordTagDataLocalMapper +import com.example.util.simpletimetracker.data_local.utils.logDataAccess import com.example.util.simpletimetracker.domain.repo.RunningRecordToRecordTagRepo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -17,7 +17,7 @@ class RunningRecordToRecordTagRepoImpl @Inject constructor( override suspend fun addRunningRecordTags(runningRecordId: Long, tagIds: List) = withContext(Dispatchers.IO) { - Timber.d("add running record tags") + logDataAccess("add running record tags") tagIds.map { mapper.map(recordId = runningRecordId, recordTagId = it) }.let { @@ -27,19 +27,19 @@ class RunningRecordToRecordTagRepoImpl @Inject constructor( override suspend fun removeAllByTagId(tagId: Long) = withContext(Dispatchers.IO) { - Timber.d("remove all by tagId") + logDataAccess("remove all by tagId") dao.deleteAllByTagId(tagId) } override suspend fun removeAllByRunningRecordId(runningRecordId: Long) = withContext(Dispatchers.IO) { - Timber.d("remove all by runningRecordId") + logDataAccess("remove all by runningRecordId") dao.deleteAllByRecordId(runningRecordId) } override suspend fun clear() = withContext(Dispatchers.IO) { - Timber.d("clear") + logDataAccess("clear") dao.clear() } } \ No newline at end of file diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/utils/PrefsUtils.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/utils/PrefsUtils.kt new file mode 100644 index 000000000..3ed551fb2 --- /dev/null +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/utils/PrefsUtils.kt @@ -0,0 +1,91 @@ +package com.example.util.simpletimetracker.data_local.utils + +import android.content.SharedPreferences +import com.example.util.simpletimetracker.data_local.repo.RepoConstants.LOG_MESSAGE_CACHE +import com.example.util.simpletimetracker.data_local.repo.RepoConstants.LOG_MESSAGE_PREFIX +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import timber.log.Timber +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +@Suppress("UNCHECKED_CAST") +inline fun SharedPreferences.delegate( + key: String, + default: T, +) = object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T = + when (default) { + is Boolean -> (getBoolean(key, default) as? T) ?: default + is Int -> (getInt(key, default) as? T) ?: default + is Long -> (getLong(key, default) as? T) ?: default + is String -> (getString(key, default) as? T) ?: default + is Set<*> -> (getStringSet(key, default as? Set)?.toSet() as? T) ?: default + else -> throw IllegalArgumentException( + "Prefs delegate not implemented for class ${(default as Any?)?.javaClass}", + ) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = with(edit()) { + when (value) { + is Boolean -> putBoolean(key, value) + is Int -> putInt(key, value) + is Long -> putLong(key, value) + is String -> putString(key, value) + is Set<*> -> putStringSet(key, value as? Set) + else -> throw IllegalArgumentException( + "Prefs delegate not implemented for class ${(default as Any?)?.javaClass}", + ) + } + apply() + } +} + +internal suspend inline fun Mutex.withLockedCache( + logMessage: String, + crossinline accessCache: () -> T? = { null }, + crossinline accessSource: suspend () -> T, +): T { + return withLockedCache( + logMessage = logMessage, + accessCache = accessCache, + accessSource = accessSource, + afterSourceAccess = {}, + ) +} + +internal suspend inline fun Mutex.withLockedCache( + logMessage: String, + crossinline accessCache: () -> T? = { null }, + crossinline accessSource: suspend () -> T, + crossinline afterSourceAccess: suspend (T) -> Unit, +): T { + return withContext(Dispatchers.IO) { + this@withLockedCache.withLock { + accessCache()?.let { + logDataAccess("$logMessage ($LOG_MESSAGE_CACHE)") + it + } ?: run { + logDataAccess(logMessage) + accessSource().also { afterSourceAccess(it) } + } + } + } +} + +/** + * Inlined for message tag to be an actual class name at call site. + */ +@Suppress("NOTHING_TO_INLINE") +internal inline fun logDataAccess(logMessage: String) { + Timber.d("$LOG_MESSAGE_PREFIX $logMessage") +} + +/** + * Produces a new list from original list by removing elements satisfying filter block. + */ +inline fun List.removeIf(crossinline filter: (T) -> Boolean): List { + return this.toMutableList().apply { removeIf { filter(it) } } +} \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeCategoryInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeCategoryInteractor.kt index 1ff675c1e..c45882fdd 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeCategoryInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeCategoryInteractor.kt @@ -12,7 +12,7 @@ class RecordTypeCategoryInteractor @Inject constructor( return recordTypeCategoryRepo.getAll() } - suspend fun getCategories(typeId: Long): List { + suspend fun getCategories(typeId: Long): Set { return recordTypeCategoryRepo.getCategoryIdsByType(typeId) } @@ -24,7 +24,7 @@ class RecordTypeCategoryInteractor @Inject constructor( recordTypeCategoryRepo.removeCategories(typeId, categoryIds) } - suspend fun getTypes(categoryId: Long): List { + suspend fun getTypes(categoryId: Long): Set { return recordTypeCategoryRepo.getTypeIdsByCategory(categoryId) } diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeGoalInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeGoalInteractor.kt index 16e33439b..2bc4d24b6 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeGoalInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeGoalInteractor.kt @@ -28,10 +28,6 @@ class RecordTypeGoalInteractor @Inject constructor( return repo.getByCategory(categoryId) } - suspend fun getByCategories(categoryIds: List): List { - return repo.getByCategories(categoryIds) - } - suspend fun add(recordTypeGoal: RecordTypeGoal) { repo.add(recordTypeGoal) } diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/repo/RecordTypeCategoryRepo.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/repo/RecordTypeCategoryRepo.kt index 936b4280a..9f58c7117 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/repo/RecordTypeCategoryRepo.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/repo/RecordTypeCategoryRepo.kt @@ -9,9 +9,9 @@ interface RecordTypeCategoryRepo { suspend fun getCategoriesByType(typeId: Long): List - suspend fun getCategoryIdsByType(typeId: Long): List + suspend fun getCategoryIdsByType(typeId: Long): Set - suspend fun getTypeIdsByCategory(categoryId: Long): List + suspend fun getTypeIdsByCategory(categoryId: Long): Set suspend fun add(recordTypeCategory: RecordTypeCategory) diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/repo/RecordTypeGoalRepo.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/repo/RecordTypeGoalRepo.kt index 250bc73a9..c61421449 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/repo/RecordTypeGoalRepo.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/repo/RecordTypeGoalRepo.kt @@ -14,8 +14,6 @@ interface RecordTypeGoalRepo { suspend fun getByCategory(categoryId: Long): List - suspend fun getByCategories(categoryIds: List): List - suspend fun add(recordTypeGoal: RecordTypeGoal): Long suspend fun remove(id: Long) diff --git a/features/feature_change_category/src/main/java/com/example/util/simpletimetracker/feature_change_category/viewModel/ChangeCategoryViewModel.kt b/features/feature_change_category/src/main/java/com/example/util/simpletimetracker/feature_change_category/viewModel/ChangeCategoryViewModel.kt index 0be142bcb..8cafe6503 100644 --- a/features/feature_change_category/src/main/java/com/example/util/simpletimetracker/feature_change_category/viewModel/ChangeCategoryViewModel.kt +++ b/features/feature_change_category/src/main/java/com/example/util/simpletimetracker/feature_change_category/viewModel/ChangeCategoryViewModel.kt @@ -88,7 +88,7 @@ class ChangeCategoryViewModel @Inject constructor( val keyboardVisibility: LiveData by lazy { MutableLiveData(categoryId == 0L) } private val categoryId: Long get() = (extra as? ChangeTagData.Change)?.id.orZero() - private var initialTypes: List = emptyList() + private var initialTypes: Set = emptySet() private var newName: String = "" private var newColor: AppColor = AppColor(colorId = (0..ColorMapper.colorsNumber).random(), colorInt = "") private var newTypes: MutableList = mutableListOf() diff --git a/features/feature_change_record_type/src/main/java/com/example/util/simpletimetracker/feature_change_record_type/viewModel/ChangeRecordTypeViewModel.kt b/features/feature_change_record_type/src/main/java/com/example/util/simpletimetracker/feature_change_record_type/viewModel/ChangeRecordTypeViewModel.kt index 084718e88..6b2f58de9 100644 --- a/features/feature_change_record_type/src/main/java/com/example/util/simpletimetracker/feature_change_record_type/viewModel/ChangeRecordTypeViewModel.kt +++ b/features/feature_change_record_type/src/main/java/com/example/util/simpletimetracker/feature_change_record_type/viewModel/ChangeRecordTypeViewModel.kt @@ -128,7 +128,7 @@ class ChangeRecordTypeViewModel @Inject constructor( private val recordTypeId: Long get() = (extra as? ChangeRecordTypeParams.Change)?.id.orZero() private var iconType: IconType = IconType.IMAGE - private var initialCategories: List = emptyList() + private var initialCategories: Set = emptySet() private var newName: String = "" private var newIconName: String = "" private var newCategories: MutableList = mutableListOf()