A fork of Simple Time Tracker with WearOS support. (Now integrated upstream)
0

Configure Feed

Select the types of activity you want to include in your feed.

add data cache

razeeman (Dec 23, 2023, 2:46 PM +0300) 222e4300 df3a13af

+599 -442
-1
app/src/androidTest/java/com/example/util/simpletimetracker/ChangeCategoryTest.kt
··· 17 17 import com.example.util.simpletimetracker.utils.clickOnViewWithText 18 18 import com.example.util.simpletimetracker.utils.longClickOnView 19 19 import com.example.util.simpletimetracker.utils.scrollRecyclerToView 20 - import com.example.util.simpletimetracker.utils.tryAction 21 20 import com.example.util.simpletimetracker.utils.typeTextIntoView 22 21 import com.example.util.simpletimetracker.utils.withCardColor 23 22 import dagger.hilt.android.testing.HiltAndroidTest
-4
data_local/src/main/java/com/example/util/simpletimetracker/data_local/database/RecordTypeGoalDao.kt
··· 27 27 @Query("SELECT * FROM recordTypeGoals WHERE category_id = :categoryId") 28 28 suspend fun getByCategory(categoryId: Long): List<RecordTypeGoalDBO> 29 29 30 - @Transaction 31 - @Query("SELECT * FROM recordTypeGoals WHERE category_id IN (:categoryIds)") 32 - suspend fun getByCategories(categoryIds: List<Long>): List<RecordTypeGoalDBO> 33 - 34 30 @Insert(onConflict = OnConflictStrategy.REPLACE) 35 31 suspend fun insert(recordTypeGoal: RecordTypeGoalDBO): Long 36 32
-37
data_local/src/main/java/com/example/util/simpletimetracker/data_local/extension/PrefsExtensions.kt
··· 1 - package com.example.util.simpletimetracker.data_local.extension 2 - 3 - import android.content.SharedPreferences 4 - import kotlin.properties.ReadWriteProperty 5 - import kotlin.reflect.KProperty 6 - 7 - @Suppress("UNCHECKED_CAST") 8 - inline fun <reified T> SharedPreferences.delegate( 9 - key: String, 10 - default: T 11 - ) = object : ReadWriteProperty<Any?, T> { 12 - override fun getValue(thisRef: Any?, property: KProperty<*>): T = 13 - when (default) { 14 - is Boolean -> (getBoolean(key, default) as? T) ?: default 15 - is Int -> (getInt(key, default) as? T) ?: default 16 - is Long -> (getLong(key, default) as? T) ?: default 17 - is String -> (getString(key, default) as? T) ?: default 18 - is Set<*> -> (getStringSet(key, default as? Set<String>)?.toSet() as? T) ?: default 19 - else -> throw IllegalArgumentException( 20 - "Prefs delegate not implemented for class ${(default as Any?)?.javaClass}" 21 - ) 22 - } 23 - 24 - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = with(edit()) { 25 - when (value) { 26 - is Boolean -> putBoolean(key, value) 27 - is Int -> putInt(key, value) 28 - is Long -> putLong(key, value) 29 - is String -> putString(key, value) 30 - is Set<*> -> putStringSet(key, value as? Set<String>) 31 - else -> throw IllegalArgumentException( 32 - "Prefs delegate not implemented for class ${(default as Any?)?.javaClass}" 33 - ) 34 - } 35 - apply() 36 - } 37 - }
+37 -31
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/ActivityFilterRepoImpl.kt
··· 1 1 package com.example.util.simpletimetracker.data_local.repo 2 2 3 3 import com.example.util.simpletimetracker.data_local.database.ActivityFilterDao 4 + import com.example.util.simpletimetracker.data_local.utils.withLockedCache 4 5 import com.example.util.simpletimetracker.data_local.mapper.ActivityFilterDataLocalMapper 6 + import com.example.util.simpletimetracker.data_local.utils.removeIf 5 7 import com.example.util.simpletimetracker.domain.model.ActivityFilter 6 8 import com.example.util.simpletimetracker.domain.repo.ActivityFilterRepo 7 - import kotlinx.coroutines.Dispatchers 8 - import kotlinx.coroutines.withContext 9 - import timber.log.Timber 9 + import kotlinx.coroutines.sync.Mutex 10 10 import javax.inject.Inject 11 11 import javax.inject.Singleton 12 12 ··· 16 16 private val activityFilterDataLocalMapper: ActivityFilterDataLocalMapper, 17 17 ) : ActivityFilterRepo { 18 18 19 - override suspend fun getAll(): List<ActivityFilter> = withContext(Dispatchers.IO) { 20 - Timber.d("getAll") 21 - activityFilterDao.getAll() 22 - .map(activityFilterDataLocalMapper::map) 23 - } 19 + private var cache: List<ActivityFilter>? = null 20 + private val mutex: Mutex = Mutex() 24 21 25 - override suspend fun get(id: Long): ActivityFilter? = withContext(Dispatchers.IO) { 26 - Timber.d("get") 27 - activityFilterDao.get(id) 28 - ?.let(activityFilterDataLocalMapper::map) 29 - } 22 + override suspend fun getAll(): List<ActivityFilter> = mutex.withLockedCache( 23 + logMessage = "getAll", 24 + accessCache = { cache }, 25 + accessSource = { activityFilterDao.getAll().map(activityFilterDataLocalMapper::map) }, 26 + afterSourceAccess = { cache = it }, 27 + ) 30 28 31 - override suspend fun add(activityFilter: ActivityFilter): Long = withContext(Dispatchers.IO) { 32 - Timber.d("add") 33 - return@withContext activityFilterDao.insert( 34 - activityFilter.let(activityFilterDataLocalMapper::map) 35 - ) 36 - } 29 + override suspend fun get(id: Long): ActivityFilter? = mutex.withLockedCache( 30 + logMessage = "get", 31 + accessCache = { cache?.firstOrNull { it.id == id } }, 32 + accessSource = { activityFilterDao.get(id)?.let(activityFilterDataLocalMapper::map) }, 33 + ) 37 34 38 - override suspend fun changeSelected(id: Long, selected: Boolean) = withContext(Dispatchers.IO) { 39 - Timber.d("changeSelected") 40 - activityFilterDao.changeSelected(id, if (selected) 1 else 0) 41 - } 35 + override suspend fun add(activityFilter: ActivityFilter): Long = mutex.withLockedCache( 36 + logMessage = "add", 37 + accessSource = { activityFilterDao.insert(activityFilter.let(activityFilterDataLocalMapper::map)) }, 38 + afterSourceAccess = { cache = null }, 39 + ) 42 40 43 - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { 44 - Timber.d("remove") 45 - activityFilterDao.delete(id) 46 - } 41 + override suspend fun changeSelected(id: Long, selected: Boolean) = mutex.withLockedCache( 42 + logMessage = "changeSelected", 43 + accessSource = { activityFilterDao.changeSelected(id, if (selected) 1 else 0) }, 44 + afterSourceAccess = { cache = cache?.map { if (it.id == id) it.copy(selected = selected) else it } }, 45 + ) 47 46 48 - override suspend fun clear() = withContext(Dispatchers.IO) { 49 - Timber.d("clear") 50 - activityFilterDao.clear() 51 - } 47 + override suspend fun remove(id: Long) = mutex.withLockedCache( 48 + logMessage = "remove", 49 + accessSource = { activityFilterDao.delete(id) }, 50 + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, 51 + ) 52 + 53 + override suspend fun clear() = mutex.withLockedCache( 54 + logMessage = "clear", 55 + accessSource = { activityFilterDao.clear() }, 56 + afterSourceAccess = { cache = null }, 57 + ) 52 58 }
+33 -27
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/CategoryRepoImpl.kt
··· 1 1 package com.example.util.simpletimetracker.data_local.repo 2 2 3 3 import com.example.util.simpletimetracker.data_local.database.CategoryDao 4 + import com.example.util.simpletimetracker.data_local.utils.withLockedCache 4 5 import com.example.util.simpletimetracker.data_local.mapper.CategoryDataLocalMapper 6 + import com.example.util.simpletimetracker.data_local.utils.removeIf 5 7 import com.example.util.simpletimetracker.domain.model.Category 6 8 import com.example.util.simpletimetracker.domain.repo.CategoryRepo 7 - import kotlinx.coroutines.Dispatchers 8 - import kotlinx.coroutines.withContext 9 - import timber.log.Timber 9 + import kotlinx.coroutines.sync.Mutex 10 10 import javax.inject.Inject 11 11 import javax.inject.Singleton 12 12 13 13 @Singleton 14 14 class CategoryRepoImpl @Inject constructor( 15 15 private val categoryDao: CategoryDao, 16 - private val categoryDataLocalMapper: CategoryDataLocalMapper 16 + private val categoryDataLocalMapper: CategoryDataLocalMapper, 17 17 ) : CategoryRepo { 18 18 19 - override suspend fun getAll(): List<Category> = withContext(Dispatchers.IO) { 20 - Timber.d("getAll") 21 - categoryDao.getAll() 22 - .map(categoryDataLocalMapper::map) 23 - } 19 + private var cache: List<Category>? = null 20 + private val mutex: Mutex = Mutex() 24 21 25 - override suspend fun get(id: Long): Category? = withContext(Dispatchers.IO) { 26 - Timber.d("get id") 27 - categoryDao.get(id)?.let(categoryDataLocalMapper::map) 28 - } 22 + override suspend fun getAll(): List<Category> = mutex.withLockedCache( 23 + logMessage = "getAll", 24 + accessCache = { cache }, 25 + accessSource = { categoryDao.getAll().map(categoryDataLocalMapper::map) }, 26 + afterSourceAccess = { cache = it }, 27 + ) 29 28 30 - override suspend fun add(category: Category): Long = withContext(Dispatchers.IO) { 31 - Timber.d("add") 32 - return@withContext categoryDao.insert( 33 - category.let(categoryDataLocalMapper::map) 34 - ) 35 - } 29 + override suspend fun get(id: Long): Category? = mutex.withLockedCache( 30 + logMessage = "get id", 31 + accessCache = { cache?.firstOrNull { it.id == id } }, 32 + accessSource = { categoryDao.get(id)?.let(categoryDataLocalMapper::map) }, 33 + ) 36 34 37 - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { 38 - Timber.d("remove") 39 - categoryDao.delete(id) 40 - } 35 + override suspend fun add(category: Category): Long = mutex.withLockedCache( 36 + logMessage = "add", 37 + accessSource = { categoryDao.insert(category.let(categoryDataLocalMapper::map)) }, 38 + afterSourceAccess = { cache = null }, 39 + ) 41 40 42 - override suspend fun clear() = withContext(Dispatchers.IO) { 43 - Timber.d("clear") 44 - categoryDao.clear() 45 - } 41 + override suspend fun remove(id: Long) = mutex.withLockedCache( 42 + logMessage = "remove", 43 + accessSource = { categoryDao.delete(id) }, 44 + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, 45 + ) 46 + 47 + override suspend fun clear() = mutex.withLockedCache( 48 + logMessage = "clear", 49 + accessSource = { categoryDao.clear() }, 50 + afterSourceAccess = { cache = null }, 51 + ) 46 52 }
+9 -9
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/FavouriteCommentRepoImpl.kt
··· 2 2 3 3 import com.example.util.simpletimetracker.data_local.database.FavouriteCommentDao 4 4 import com.example.util.simpletimetracker.data_local.mapper.FavouriteCommentDataLocalMapper 5 + import com.example.util.simpletimetracker.data_local.utils.logDataAccess 5 6 import com.example.util.simpletimetracker.domain.model.FavouriteComment 6 7 import com.example.util.simpletimetracker.domain.repo.FavouriteCommentRepo 8 + import kotlinx.coroutines.Dispatchers 9 + import kotlinx.coroutines.withContext 7 10 import javax.inject.Inject 8 11 import javax.inject.Singleton 9 - import kotlinx.coroutines.Dispatchers 10 - import kotlinx.coroutines.withContext 11 - import timber.log.Timber 12 12 13 13 @Singleton 14 14 class FavouriteCommentRepoImpl @Inject constructor( ··· 17 17 ) : FavouriteCommentRepo { 18 18 19 19 override suspend fun getAll(): List<FavouriteComment> = withContext(Dispatchers.IO) { 20 - Timber.d("getAll") 20 + logDataAccess("getAll") 21 21 favouriteCommentDao.getAll().map(FavouriteCommentDataLocalMapper::map) 22 22 } 23 23 24 24 override suspend fun get(id: Long): FavouriteComment? = withContext(Dispatchers.IO) { 25 - Timber.d("get id") 25 + logDataAccess("get id") 26 26 favouriteCommentDao.get(id)?.let(FavouriteCommentDataLocalMapper::map) 27 27 } 28 28 29 29 override suspend fun get(text: String): FavouriteComment? = withContext(Dispatchers.IO) { 30 - Timber.d("get text") 30 + logDataAccess("get text") 31 31 favouriteCommentDao.get(text)?.let(FavouriteCommentDataLocalMapper::map) 32 32 } 33 33 34 34 override suspend fun add(comment: FavouriteComment): Long = withContext(Dispatchers.IO) { 35 - Timber.d("add") 35 + logDataAccess("add") 36 36 return@withContext favouriteCommentDao.insert( 37 37 comment.let(FavouriteCommentDataLocalMapper::map) 38 38 ) 39 39 } 40 40 41 41 override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { 42 - Timber.d("remove") 42 + logDataAccess("remove") 43 43 favouriteCommentDao.delete(id) 44 44 } 45 45 46 46 override suspend fun clear() = withContext(Dispatchers.IO) { 47 - Timber.d("clear") 47 + logDataAccess("clear") 48 48 favouriteCommentDao.clear() 49 49 } 50 50 }
+1 -1
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/PrefsRepoImpl.kt
··· 1 1 package com.example.util.simpletimetracker.data_local.repo 2 2 3 3 import android.content.SharedPreferences 4 - import com.example.util.simpletimetracker.data_local.extension.delegate 4 + import com.example.util.simpletimetracker.data_local.utils.delegate 5 5 import com.example.util.simpletimetracker.domain.extension.orZero 6 6 import com.example.util.simpletimetracker.domain.model.ChartFilterType 7 7 import com.example.util.simpletimetracker.domain.model.QuickSettingsWidgetType
+99 -62
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordRepoImpl.kt
··· 1 1 package com.example.util.simpletimetracker.data_local.repo 2 2 3 + import androidx.collection.LruCache 3 4 import com.example.util.simpletimetracker.data_local.database.RecordDao 4 5 import com.example.util.simpletimetracker.data_local.mapper.RecordDataLocalMapper 6 + import com.example.util.simpletimetracker.data_local.utils.logDataAccess 7 + import com.example.util.simpletimetracker.data_local.utils.withLockedCache 5 8 import com.example.util.simpletimetracker.domain.model.Range 6 9 import com.example.util.simpletimetracker.domain.model.Record 7 10 import com.example.util.simpletimetracker.domain.repo.RecordRepo 8 11 import kotlinx.coroutines.Dispatchers 12 + import kotlinx.coroutines.sync.Mutex 9 13 import kotlinx.coroutines.withContext 10 - import timber.log.Timber 11 14 import javax.inject.Inject 12 15 import javax.inject.Singleton 13 16 ··· 17 20 private val recordDataLocalMapper: RecordDataLocalMapper, 18 21 ) : RecordRepo { 19 22 20 - override suspend fun isEmpty(): Boolean = withContext(Dispatchers.IO) { 21 - Timber.d("isEmpty") 22 - return@withContext recordDao.isEmpty() == 0L 23 - } 23 + private var getFromRangeCache = LruCache<GetFromRangeKey, List<Record>>(10) 24 + private var getFromRangeByTypeCache = LruCache<GetFromRangeByTypeKey, List<Record>>(1) 25 + private var isEmpty: Boolean? = null 26 + private val mutex: Mutex = Mutex() 27 + 28 + override suspend fun isEmpty(): Boolean = mutex.withLockedCache( 29 + logMessage = "isEmpty", 30 + accessCache = { isEmpty }, 31 + accessSource = { recordDao.isEmpty() == 0L }, 32 + afterSourceAccess = { isEmpty = it }, 33 + ) 24 34 25 35 override suspend fun getAll(): List<Record> = withContext(Dispatchers.IO) { 26 - Timber.d("getAll") 27 - recordDao.getAll() 28 - .map(recordDataLocalMapper::map) 36 + logDataAccess("getAll") 37 + recordDao.getAll().map(recordDataLocalMapper::map) 29 38 } 30 39 31 40 override suspend fun getByType(typeIds: List<Long>): List<Record> = withContext(Dispatchers.IO) { 32 - Timber.d("getByType") 33 - recordDao.getByType(typeIds) 34 - .map(recordDataLocalMapper::map) 41 + logDataAccess("getByType") 42 + recordDao.getByType(typeIds).map(recordDataLocalMapper::map) 35 43 } 36 44 37 45 override suspend fun getByTypeWithAnyComment(typeIds: List<Long>): List<Record> = withContext(Dispatchers.IO) { 38 - Timber.d("getByTypeWithAnyComment") 39 - recordDao.getByTypeWithAnyComment(typeIds) 40 - .map(recordDataLocalMapper::map) 46 + logDataAccess("getByTypeWithAnyComment") 47 + recordDao.getByTypeWithAnyComment(typeIds).map(recordDataLocalMapper::map) 41 48 } 42 49 43 50 override suspend fun searchComment( 44 51 text: String, 45 52 ): List<Record> = withContext(Dispatchers.IO) { 46 - Timber.d("searchComment") 47 - recordDao.searchComment(text) 48 - .map(recordDataLocalMapper::map) 53 + logDataAccess("searchComment") 54 + recordDao.searchComment(text).map(recordDataLocalMapper::map) 49 55 } 50 56 51 57 override suspend fun searchByTypeWithComment( 52 58 typeIds: List<Long>, 53 59 text: String, 54 60 ): List<Record> = withContext(Dispatchers.IO) { 55 - Timber.d("searchByTypeWithComment") 56 - recordDao.searchByTypeWithComment(typeIds, text) 57 - .map(recordDataLocalMapper::map) 61 + logDataAccess("searchByTypeWithComment") 62 + recordDao.searchByTypeWithComment(typeIds, text).map(recordDataLocalMapper::map) 58 63 } 59 64 60 65 override suspend fun searchAnyComments(): List<Record> = withContext(Dispatchers.IO) { 61 - Timber.d("searchAnyComments") 62 - recordDao.searchAnyComments() 63 - .map(recordDataLocalMapper::map) 66 + logDataAccess("searchAnyComments") 67 + recordDao.searchAnyComments().map(recordDataLocalMapper::map) 64 68 } 65 69 66 70 override suspend fun get(id: Long): Record? = withContext(Dispatchers.IO) { 67 - Timber.d("get") 68 - recordDao.get(id) 69 - ?.let(recordDataLocalMapper::map) 71 + logDataAccess("get") 72 + recordDao.get(id)?.let(recordDataLocalMapper::map) 70 73 } 71 74 72 - override suspend fun getFromRange(range: Range): List<Record> = 73 - withContext(Dispatchers.IO) { 74 - Timber.d("getFromRange") 75 - recordDao.getFromRange(range.timeStarted, range.timeEnded) 76 - .map(recordDataLocalMapper::map) 77 - } 75 + override suspend fun getFromRange(range: Range): List<Record> { 76 + val cacheKey = GetFromRangeKey(range) 77 + return mutex.withLockedCache( 78 + logMessage = "getFromRange", 79 + accessCache = { getFromRangeCache.get(cacheKey) }, 80 + accessSource = { 81 + recordDao.getFromRange(range.timeStarted, range.timeEnded) 82 + .map(recordDataLocalMapper::map) 83 + }, 84 + afterSourceAccess = { getFromRangeCache.put(cacheKey, it) }, 85 + ) 86 + } 78 87 79 - override suspend fun getFromRangeByType(typeIds: List<Long>, range: Range): List<Record> = 80 - withContext(Dispatchers.IO) { 81 - Timber.d("getFromRangeByType") 82 - recordDao.getFromRangeByType(typeIds, range.timeStarted, range.timeEnded) 83 - .map(recordDataLocalMapper::map) 84 - } 88 + override suspend fun getFromRangeByType(typeIds: List<Long>, range: Range): List<Record> { 89 + val cacheKey = GetFromRangeByTypeKey(typeIds, range) 90 + return mutex.withLockedCache( 91 + logMessage = "getFromRangeByType", 92 + accessCache = { getFromRangeByTypeCache.get(cacheKey) }, 93 + accessSource = { 94 + recordDao.getFromRangeByType( 95 + typesIds = typeIds, 96 + start = range.timeStarted, 97 + end = range.timeEnded, 98 + ).map(recordDataLocalMapper::map) 99 + }, 100 + afterSourceAccess = { getFromRangeByTypeCache.put(cacheKey, it) }, 101 + ) 102 + } 85 103 86 104 override suspend fun getPrev(timeStarted: Long, limit: Long): List<Record> = 87 105 withContext(Dispatchers.IO) { 88 - Timber.d("getPrev") 106 + logDataAccess("getPrev") 89 107 recordDao.getPrev(timeStarted, limit) 90 108 .map(recordDataLocalMapper::map) 91 109 } 92 110 93 111 override suspend fun getNext(timeEnded: Long): Record? = 94 112 withContext(Dispatchers.IO) { 95 - Timber.d("getNext") 113 + logDataAccess("getNext") 96 114 recordDao.getNext(timeEnded) 97 115 ?.let(recordDataLocalMapper::map) 98 116 } 99 117 100 - override suspend fun add(record: Record): Long = withContext(Dispatchers.IO) { 101 - Timber.d("add") 102 - // Drop milliseconds. 103 - val adjustedRecord = record.copy( 104 - timeStarted = record.timeStarted / 1000 * 1000, 105 - timeEnded = record.timeEnded / 1000 * 1000, 106 - ) 107 - return@withContext recordDao.insert( 108 - adjustedRecord.let(recordDataLocalMapper::map) 109 - ) 110 - } 118 + override suspend fun add(record: Record): Long = mutex.withLockedCache( 119 + logMessage = "add", 120 + accessSource = { 121 + // Drop milliseconds. 122 + val adjustedRecord = record.copy( 123 + timeStarted = record.timeStarted / 1000 * 1000, 124 + timeEnded = record.timeEnded / 1000 * 1000, 125 + ) 126 + recordDao.insert(adjustedRecord.let(recordDataLocalMapper::map)) 127 + }, 128 + afterSourceAccess = { clearCache() }, 129 + ) 111 130 112 - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { 113 - Timber.d("remove") 114 - recordDao.delete(id) 115 - } 131 + override suspend fun remove(id: Long) = mutex.withLockedCache( 132 + logMessage = "remove", 133 + accessSource = { recordDao.delete(id) }, 134 + afterSourceAccess = { clearCache() }, 135 + ) 116 136 117 - override suspend fun removeByType(typeId: Long) = withContext(Dispatchers.IO) { 118 - Timber.d("removeByType") 119 - recordDao.deleteByType(typeId) 120 - } 137 + override suspend fun removeByType(typeId: Long) = mutex.withLockedCache( 138 + logMessage = "removeByType", 139 + accessSource = { recordDao.deleteByType(typeId) }, 140 + afterSourceAccess = { clearCache() }, 141 + ) 121 142 122 - override suspend fun clear() = withContext(Dispatchers.IO) { 123 - Timber.d("clear") 124 - recordDao.clear() 143 + override suspend fun clear() = mutex.withLockedCache( 144 + logMessage = "clear", 145 + accessSource = { recordDao.clear() }, 146 + afterSourceAccess = { clearCache() }, 147 + ) 148 + 149 + private fun clearCache() { 150 + getFromRangeCache.evictAll() 151 + getFromRangeByTypeCache.evictAll() 152 + isEmpty = null 125 153 } 154 + 155 + private data class GetFromRangeByTypeKey( 156 + val typeIds: List<Long>, 157 + val range: Range, 158 + ) 159 + 160 + private data class GetFromRangeKey( 161 + val range: Range, 162 + ) 126 163 }
+68 -52
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTagRepoImpl.kt
··· 1 1 package com.example.util.simpletimetracker.data_local.repo 2 2 3 3 import com.example.util.simpletimetracker.data_local.database.RecordTagDao 4 + import com.example.util.simpletimetracker.data_local.utils.withLockedCache 4 5 import com.example.util.simpletimetracker.data_local.mapper.RecordTagDataLocalMapper 6 + import com.example.util.simpletimetracker.data_local.utils.removeIf 5 7 import com.example.util.simpletimetracker.domain.model.RecordTag 6 8 import com.example.util.simpletimetracker.domain.repo.RecordTagRepo 7 - import kotlinx.coroutines.Dispatchers 8 - import kotlinx.coroutines.withContext 9 - import timber.log.Timber 9 + import kotlinx.coroutines.sync.Mutex 10 10 import javax.inject.Inject 11 11 import javax.inject.Singleton 12 12 13 13 @Singleton 14 14 class RecordTagRepoImpl @Inject constructor( 15 15 private val dao: RecordTagDao, 16 - private val mapper: RecordTagDataLocalMapper 16 + private val mapper: RecordTagDataLocalMapper, 17 17 ) : RecordTagRepo { 18 18 19 - override suspend fun isEmpty(): Boolean = withContext(Dispatchers.IO) { 20 - Timber.d("isEmpty") 21 - return@withContext dao.isEmpty() == 0L 22 - } 19 + private var cache: List<RecordTag>? = null 20 + private val mutex: Mutex = Mutex() 23 21 24 - override suspend fun getAll(): List<RecordTag> = withContext(Dispatchers.IO) { 25 - Timber.d("getAll") 26 - dao.getAll().map(mapper::map) 27 - } 22 + override suspend fun isEmpty(): Boolean = mutex.withLockedCache( 23 + logMessage = "isEmpty", 24 + accessCache = { cache?.isEmpty() }, 25 + accessSource = { dao.isEmpty() == 0L }, 26 + ) 28 27 29 - override suspend fun get(id: Long): RecordTag? = withContext(Dispatchers.IO) { 30 - Timber.d("get") 31 - dao.get(id)?.let(mapper::map) 32 - } 28 + override suspend fun getAll(): List<RecordTag> = mutex.withLockedCache( 29 + logMessage = "getAll", 30 + accessCache = { cache }, 31 + accessSource = { dao.getAll().map(mapper::map) }, 32 + afterSourceAccess = { cache = it }, 33 + ) 33 34 34 - override suspend fun getByType(typeId: Long): List<RecordTag> = withContext(Dispatchers.IO) { 35 - Timber.d("getByType") 36 - dao.getByType(typeId).map(mapper::map) 37 - } 35 + override suspend fun get(id: Long): RecordTag? = mutex.withLockedCache( 36 + logMessage = "get", 37 + accessCache = { cache?.firstOrNull { it.id == id } }, 38 + accessSource = { dao.get(id)?.let(mapper::map) }, 39 + ) 38 40 39 - override suspend fun getUntyped(): List<RecordTag> = withContext(Dispatchers.IO) { 40 - Timber.d("getUntyped") 41 - dao.getUntyped().map(mapper::map) 42 - } 41 + override suspend fun getByType(typeId: Long): List<RecordTag> = mutex.withLockedCache( 42 + logMessage = "getByType", 43 + accessCache = { cache?.filter { it.typeId == typeId } }, 44 + accessSource = { dao.getByType(typeId).map(mapper::map) }, 45 + ) 43 46 44 - override suspend fun getByTypeOrUntyped(typeId: Long): List<RecordTag> = withContext(Dispatchers.IO) { 45 - Timber.d("getByTypeOrUntyped") 46 - dao.getByTypeOrUntyped(typeId).map(mapper::map) 47 - } 47 + override suspend fun getUntyped(): List<RecordTag> = mutex.withLockedCache( 48 + logMessage = "getUntyped", 49 + accessCache = { cache?.filter { it.typeId == 0L } }, 50 + accessSource = { dao.getUntyped().map(mapper::map) }, 51 + ) 48 52 49 - override suspend fun add(tag: RecordTag): Long = withContext(Dispatchers.IO) { 50 - Timber.d("add") 51 - dao.insert(tag.let(mapper::map)) 52 - } 53 + override suspend fun getByTypeOrUntyped(typeId: Long): List<RecordTag> = mutex.withLockedCache( 54 + logMessage = "getByTypeOrUntyped", 55 + accessCache = { cache?.filter { it.typeId == 0L || it.typeId == typeId } }, 56 + accessSource = { dao.getByTypeOrUntyped(typeId).map(mapper::map) }, 57 + ) 53 58 54 - override suspend fun archive(id: Long) = withContext(Dispatchers.IO) { 55 - Timber.d("archive") 56 - dao.archive(id) 57 - } 59 + override suspend fun add(tag: RecordTag): Long = mutex.withLockedCache( 60 + logMessage = "add", 61 + accessSource = { dao.insert(tag.let(mapper::map)) }, 62 + afterSourceAccess = { cache = null }, 63 + ) 58 64 59 - override suspend fun restore(id: Long) = withContext(Dispatchers.IO) { 60 - Timber.d("restore") 61 - dao.restore(id) 62 - } 65 + override suspend fun archive(id: Long) = mutex.withLockedCache( 66 + logMessage = "archive", 67 + accessSource = { dao.archive(id) }, 68 + afterSourceAccess = { cache = cache?.map { if (it.id == id) it.copy(archived = true) else it } }, 69 + ) 63 70 64 - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { 65 - Timber.d("remove") 66 - dao.delete(id) 67 - } 71 + override suspend fun restore(id: Long) = mutex.withLockedCache( 72 + logMessage = "restore", 73 + accessSource = { dao.restore(id) }, 74 + afterSourceAccess = { cache = cache?.map { if (it.id == id) it.copy(archived = false) else it } }, 75 + ) 68 76 69 - override suspend fun removeByType(typeId: Long) = withContext(Dispatchers.IO) { 70 - Timber.d("removeByType") 71 - dao.deleteByType(typeId) 72 - } 77 + override suspend fun remove(id: Long) = mutex.withLockedCache( 78 + logMessage = "remove", 79 + accessSource = { dao.delete(id) }, 80 + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, 81 + ) 73 82 74 - override suspend fun clear() = withContext(Dispatchers.IO) { 75 - Timber.d("clear") 76 - dao.clear() 77 - } 83 + override suspend fun removeByType(typeId: Long) = mutex.withLockedCache( 84 + logMessage = "removeByType", 85 + accessSource = { dao.deleteByType(typeId) }, 86 + afterSourceAccess = { cache = cache?.removeIf { it.typeId == typeId } }, 87 + ) 88 + 89 + override suspend fun clear() = mutex.withLockedCache( 90 + logMessage = "clear", 91 + accessSource = { dao.clear() }, 92 + afterSourceAccess = { cache = null }, 93 + ) 78 94 }
+10 -10
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordToRecordTagRepoImpl.kt
··· 2 2 3 3 import com.example.util.simpletimetracker.data_local.database.RecordToRecordTagDao 4 4 import com.example.util.simpletimetracker.data_local.mapper.RecordToRecordTagDataLocalMapper 5 + import com.example.util.simpletimetracker.data_local.utils.logDataAccess 5 6 import com.example.util.simpletimetracker.domain.model.RecordToRecordTag 6 7 import com.example.util.simpletimetracker.domain.repo.RecordToRecordTagRepo 7 8 import kotlinx.coroutines.Dispatchers 8 9 import kotlinx.coroutines.withContext 9 - import timber.log.Timber 10 10 import javax.inject.Inject 11 11 import javax.inject.Singleton 12 12 ··· 18 18 19 19 override suspend fun getAll(): List<RecordToRecordTag> = 20 20 withContext(Dispatchers.IO) { 21 - Timber.d("get all") 21 + logDataAccess("get all") 22 22 dao.getAll().map(mapper::map) 23 23 } 24 24 25 25 override suspend fun getTagIdsByRecordId(recordId: Long): List<Long> = 26 26 withContext(Dispatchers.IO) { 27 - Timber.d("get tag ids") 27 + logDataAccess("get tag ids") 28 28 dao.getTagIdsByRecordId(recordId) 29 29 } 30 30 31 31 override suspend fun getRecordIdsByTagId(tagId: Long): List<Long> = 32 32 withContext(Dispatchers.IO) { 33 - Timber.d("get record ids") 33 + logDataAccess("get record ids") 34 34 dao.getRecordIdsByTagId(tagId) 35 35 } 36 36 37 37 override suspend fun add(recordToRecordTag: RecordToRecordTag) = 38 38 withContext(Dispatchers.IO) { 39 - Timber.d("add") 39 + logDataAccess("add") 40 40 recordToRecordTag 41 41 .let(mapper::map) 42 42 .let { ··· 46 46 47 47 override suspend fun addRecordTags(recordId: Long, tagIds: List<Long>) = 48 48 withContext(Dispatchers.IO) { 49 - Timber.d("add record tags") 49 + logDataAccess("add record tags") 50 50 tagIds.map { 51 51 mapper.map(recordId = recordId, recordTagId = it) 52 52 }.let { ··· 56 56 57 57 override suspend fun removeRecordTags(recordId: Long, tagIds: List<Long>) = 58 58 withContext(Dispatchers.IO) { 59 - Timber.d("remove record tags") 59 + logDataAccess("remove record tags") 60 60 tagIds.map { 61 61 mapper.map(recordId = recordId, recordTagId = it) 62 62 }.let { ··· 66 66 67 67 override suspend fun removeAllByTagId(tagId: Long) = 68 68 withContext(Dispatchers.IO) { 69 - Timber.d("remove all by tagId") 69 + logDataAccess("remove all by tagId") 70 70 dao.deleteAllByTagId(tagId) 71 71 } 72 72 73 73 override suspend fun removeAllByRecordId(recordId: Long) = 74 74 withContext(Dispatchers.IO) { 75 - Timber.d("remove all by recordId") 75 + logDataAccess("remove all by recordId") 76 76 dao.deleteAllByRecordId(recordId) 77 77 } 78 78 79 79 override suspend fun clear() = 80 80 withContext(Dispatchers.IO) { 81 - Timber.d("clear") 81 + logDataAccess("clear") 82 82 dao.clear() 83 83 } 84 84 }
+78 -74
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeCategoryRepoImpl.kt
··· 1 1 package com.example.util.simpletimetracker.data_local.repo 2 2 3 3 import com.example.util.simpletimetracker.data_local.database.RecordTypeCategoryDao 4 + import com.example.util.simpletimetracker.data_local.utils.withLockedCache 4 5 import com.example.util.simpletimetracker.data_local.mapper.CategoryDataLocalMapper 5 6 import com.example.util.simpletimetracker.data_local.mapper.RecordTypeCategoryDataLocalMapper 7 + import com.example.util.simpletimetracker.data_local.utils.removeIf 6 8 import com.example.util.simpletimetracker.domain.model.Category 7 9 import com.example.util.simpletimetracker.domain.model.RecordTypeCategory 8 10 import com.example.util.simpletimetracker.domain.repo.RecordTypeCategoryRepo 9 - import kotlinx.coroutines.Dispatchers 10 - import kotlinx.coroutines.withContext 11 - import timber.log.Timber 11 + import kotlinx.coroutines.sync.Mutex 12 12 import javax.inject.Inject 13 13 import javax.inject.Singleton 14 14 ··· 16 16 class RecordTypeCategoryRepoImpl @Inject constructor( 17 17 private val recordTypeCategoryDao: RecordTypeCategoryDao, 18 18 private val categoryDataLocalMapper: CategoryDataLocalMapper, 19 - private val recordTypeCategoryDataLocalMapper: RecordTypeCategoryDataLocalMapper 19 + private val recordTypeCategoryDataLocalMapper: RecordTypeCategoryDataLocalMapper, 20 20 ) : RecordTypeCategoryRepo { 21 21 22 - override suspend fun getAll(): List<RecordTypeCategory> = 23 - withContext(Dispatchers.IO) { 24 - Timber.d("get all") 25 - recordTypeCategoryDao.getAll() 26 - .map(recordTypeCategoryDataLocalMapper::map) 27 - } 22 + private var cache: List<RecordTypeCategory>? = null 23 + private val mutex: Mutex = Mutex() 28 24 29 - override suspend fun getCategoriesByType(typeId: Long): List<Category> = 30 - withContext(Dispatchers.IO) { 31 - Timber.d("get categories") 25 + override suspend fun getAll(): List<RecordTypeCategory> = mutex.withLockedCache( 26 + logMessage = "get all", 27 + accessCache = { cache }, 28 + accessSource = { recordTypeCategoryDao.getAll().map(recordTypeCategoryDataLocalMapper::map) }, 29 + afterSourceAccess = { cache = it }, 30 + ) 31 + 32 + override suspend fun getCategoriesByType(typeId: Long): List<Category> = mutex.withLockedCache( 33 + logMessage = "get categories", 34 + accessSource = { 32 35 recordTypeCategoryDao.getTypeWithCategories(typeId) 33 36 ?.categories 34 37 ?.map(categoryDataLocalMapper::map) 35 38 .orEmpty() 36 - } 39 + }, 40 + ) 37 41 38 - override suspend fun getCategoryIdsByType(typeId: Long): List<Long> = 39 - withContext(Dispatchers.IO) { 40 - Timber.d("get category ids") 41 - recordTypeCategoryDao.getCategoryIdsByType(typeId) 42 - } 42 + override suspend fun getCategoryIdsByType(typeId: Long): Set<Long> = mutex.withLockedCache( 43 + logMessage = "get category ids", 44 + accessCache = { cache?.filter { it.recordTypeId == typeId }?.map { it.categoryId }?.toSet() }, 45 + accessSource = { recordTypeCategoryDao.getCategoryIdsByType(typeId).toSet() }, 46 + ) 43 47 44 - override suspend fun add(recordTypeCategory: RecordTypeCategory) = 45 - withContext(Dispatchers.IO) { 46 - Timber.d("add") 48 + override suspend fun add(recordTypeCategory: RecordTypeCategory) = mutex.withLockedCache( 49 + logMessage = "add", 50 + accessSource = { 47 51 recordTypeCategory 48 52 .let(recordTypeCategoryDataLocalMapper::map) 49 - .let { 50 - recordTypeCategoryDao.insert(listOf(it)) 51 - } 52 - } 53 + .let { recordTypeCategoryDao.insert(listOf(it)) } 54 + }, 55 + afterSourceAccess = { cache = null }, 56 + ) 53 57 54 - override suspend fun addCategories(typeId: Long, categoryIds: List<Long>) = 55 - withContext(Dispatchers.IO) { 56 - Timber.d("add categories") 58 + override suspend fun addCategories(typeId: Long, categoryIds: List<Long>) = mutex.withLockedCache( 59 + logMessage = "add categories", 60 + accessSource = { 57 61 categoryIds.map { 58 62 recordTypeCategoryDataLocalMapper.map(typeId = typeId, categoryId = it) 59 - }.let { 60 - recordTypeCategoryDao.insert(it) 61 - } 62 - } 63 + }.let { recordTypeCategoryDao.insert(it) } 64 + }, 65 + afterSourceAccess = { cache = null }, 66 + ) 63 67 64 - override suspend fun removeCategories(typeId: Long, categoryIds: List<Long>) = 65 - withContext(Dispatchers.IO) { 66 - Timber.d("remove categories") 68 + override suspend fun removeCategories(typeId: Long, categoryIds: List<Long>) = mutex.withLockedCache( 69 + logMessage = "remove categories", 70 + accessSource = { 67 71 categoryIds.map { 68 72 recordTypeCategoryDataLocalMapper.map(typeId = typeId, categoryId = it) 69 - }.let { 70 - recordTypeCategoryDao.delete(it) 71 - } 72 - } 73 + }.let { recordTypeCategoryDao.delete(it) } 74 + }, 75 + afterSourceAccess = { cache = null }, 76 + ) 73 77 74 - override suspend fun getTypeIdsByCategory(categoryId: Long): List<Long> = 75 - withContext(Dispatchers.IO) { 76 - Timber.d("get type ids") 77 - recordTypeCategoryDao.getTypeIdsByCategory(categoryId) 78 - } 78 + override suspend fun getTypeIdsByCategory(categoryId: Long): Set<Long> = mutex.withLockedCache( 79 + logMessage = "get type ids", 80 + accessCache = { cache?.filter { it.categoryId == categoryId }?.map { it.recordTypeId }?.toSet() }, 81 + accessSource = { recordTypeCategoryDao.getTypeIdsByCategory(categoryId).toSet() }, 82 + ) 79 83 80 - override suspend fun addTypes(categoryId: Long, typeIds: List<Long>) = 81 - withContext(Dispatchers.IO) { 82 - Timber.d("add types") 84 + override suspend fun addTypes(categoryId: Long, typeIds: List<Long>) = mutex.withLockedCache( 85 + logMessage = "add types", 86 + accessSource = { 83 87 typeIds.map { 84 88 recordTypeCategoryDataLocalMapper.map(typeId = it, categoryId = categoryId) 85 - }.let { 86 - recordTypeCategoryDao.insert(it) 87 - } 88 - } 89 + }.let { recordTypeCategoryDao.insert(it) } 90 + }, 91 + afterSourceAccess = { cache = null }, 92 + ) 89 93 90 - override suspend fun removeTypes(categoryId: Long, typeIds: List<Long>) = 91 - withContext(Dispatchers.IO) { 92 - Timber.d("remove types") 94 + override suspend fun removeTypes(categoryId: Long, typeIds: List<Long>) = mutex.withLockedCache( 95 + logMessage = "remove types", 96 + accessSource = { 93 97 typeIds.map { 94 98 recordTypeCategoryDataLocalMapper.map(typeId = it, categoryId = categoryId) 95 - }.let { 96 - recordTypeCategoryDao.delete(it) 97 - } 98 - } 99 + }.let { recordTypeCategoryDao.delete(it) } 100 + }, 101 + afterSourceAccess = { cache = null }, 102 + ) 99 103 100 - override suspend fun removeAll(categoryId: Long) = 101 - withContext(Dispatchers.IO) { 102 - Timber.d("removeAll") 103 - recordTypeCategoryDao.deleteAll(categoryId) 104 - } 104 + override suspend fun removeAll(categoryId: Long) = mutex.withLockedCache( 105 + logMessage = "removeAll", 106 + accessSource = { recordTypeCategoryDao.deleteAll(categoryId) }, 107 + afterSourceAccess = { cache = cache?.removeIf { it.categoryId == categoryId } }, 108 + ) 105 109 106 - override suspend fun removeAllByType(typeId: Long) = 107 - withContext(Dispatchers.IO) { 108 - Timber.d("removeAllByType") 109 - recordTypeCategoryDao.deleteAllByType(typeId) 110 - } 110 + override suspend fun removeAllByType(typeId: Long) = mutex.withLockedCache( 111 + logMessage = "removeAllByType", 112 + accessSource = { recordTypeCategoryDao.deleteAllByType(typeId) }, 113 + afterSourceAccess = { cache = cache?.removeIf { it.recordTypeId == typeId } }, 114 + ) 111 115 112 - override suspend fun clear() = 113 - withContext(Dispatchers.IO) { 114 - Timber.d("clear") 115 - recordTypeCategoryDao.clear() 116 - } 116 + override suspend fun clear() = mutex.withLockedCache( 117 + logMessage = "clear", 118 + accessSource = { recordTypeCategoryDao.clear() }, 119 + afterSourceAccess = { cache = null }, 120 + ) 117 121 }
+70 -49
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeGoalRepoImpl.kt
··· 2 2 3 3 import com.example.util.simpletimetracker.data_local.database.RecordTypeGoalDao 4 4 import com.example.util.simpletimetracker.data_local.mapper.RecordTypeGoalDataLocalMapper 5 + import com.example.util.simpletimetracker.data_local.utils.removeIf 6 + import com.example.util.simpletimetracker.data_local.utils.withLockedCache 5 7 import com.example.util.simpletimetracker.domain.model.RecordTypeGoal 8 + import com.example.util.simpletimetracker.domain.model.RecordTypeGoal.IdData 6 9 import com.example.util.simpletimetracker.domain.repo.RecordTypeGoalRepo 7 10 import javax.inject.Inject 8 - import kotlinx.coroutines.Dispatchers 9 - import kotlinx.coroutines.withContext 10 - import timber.log.Timber 11 + import kotlinx.coroutines.sync.Mutex 11 12 12 13 class RecordTypeGoalRepoImpl @Inject constructor( 13 14 private val dao: RecordTypeGoalDao, 14 15 private val mapper: RecordTypeGoalDataLocalMapper, 15 16 ) : RecordTypeGoalRepo { 16 17 17 - override suspend fun getAll(): List<RecordTypeGoal> = withContext(Dispatchers.IO) { 18 - Timber.d("getAll") 19 - dao.getAll().map(mapper::map) 20 - } 18 + private var cache: List<RecordTypeGoal>? = null 19 + private val mutex: Mutex = Mutex() 21 20 22 - override suspend fun getAllTypeGoals(): List<RecordTypeGoal> = withContext(Dispatchers.IO) { 23 - Timber.d("getAllTypeGoals") 24 - dao.getAllTypeGoals().map(mapper::map) 25 - } 21 + override suspend fun getAll(): List<RecordTypeGoal> = mutex.withLockedCache( 22 + logMessage = "getAll", 23 + accessCache = { cache }, 24 + accessSource = { dao.getAll().map(mapper::map) }, 25 + afterSourceAccess = { cache = it }, 26 + ) 26 27 27 - override suspend fun getAllCategoryGoals(): List<RecordTypeGoal> = withContext(Dispatchers.IO) { 28 - Timber.d("getAllCategoryGoals") 29 - dao.getAllCategoryGoals().map(mapper::map) 30 - } 28 + override suspend fun getAllTypeGoals(): List<RecordTypeGoal> = mutex.withLockedCache( 29 + logMessage = "getAllTypeGoals", 30 + accessCache = { cache?.filter { it.isType() && it.idData.value != 0L } }, 31 + accessSource = { dao.getAllTypeGoals().map(mapper::map) }, 32 + afterSourceAccess = { initializeCache() }, 33 + ) 31 34 32 - override suspend fun getByType(typeId: Long): List<RecordTypeGoal> = withContext(Dispatchers.IO) { 33 - Timber.d("getByType") 34 - dao.getByType(typeId) 35 - .map(mapper::map) 36 - } 35 + override suspend fun getAllCategoryGoals(): List<RecordTypeGoal> = mutex.withLockedCache( 36 + logMessage = "getAllCategoryGoals", 37 + accessCache = { cache?.filter { it.isCategory() && it.idData.value != 0L } }, 38 + accessSource = { dao.getAllCategoryGoals().map(mapper::map) }, 39 + afterSourceAccess = { initializeCache() }, 40 + ) 41 + 42 + override suspend fun getByType(typeId: Long): List<RecordTypeGoal> = mutex.withLockedCache( 43 + logMessage = "getByType", 44 + accessCache = { cache?.filter { it.isType() && it.idData.value == typeId } }, 45 + accessSource = { dao.getByType(typeId).map(mapper::map) }, 46 + afterSourceAccess = { initializeCache() }, 47 + ) 48 + 49 + override suspend fun getByCategory(categoryId: Long): List<RecordTypeGoal> = mutex.withLockedCache( 50 + logMessage = "getByCategory", 51 + accessCache = { cache?.filter { it.isCategory() && it.idData.value == categoryId } }, 52 + accessSource = { dao.getByCategory(categoryId).map(mapper::map) }, 53 + afterSourceAccess = { initializeCache() }, 54 + ) 55 + 56 + override suspend fun add(recordTypeGoal: RecordTypeGoal): Long = mutex.withLockedCache( 57 + logMessage = "add", 58 + accessSource = { dao.insert(recordTypeGoal.let(mapper::map)) }, 59 + afterSourceAccess = { cache = null }, 60 + ) 37 61 38 - override suspend fun getByCategory(categoryId: Long): List<RecordTypeGoal> = withContext(Dispatchers.IO) { 39 - Timber.d("getByCategory") 40 - dao.getByCategory(categoryId) 41 - .map(mapper::map) 42 - } 62 + override suspend fun remove(id: Long) = mutex.withLockedCache( 63 + logMessage = "remove", 64 + accessSource = { dao.delete(id) }, 65 + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, 66 + ) 43 67 44 - override suspend fun getByCategories(categoryIds: List<Long>): List<RecordTypeGoal> = withContext(Dispatchers.IO) { 45 - Timber.d("getByCategories") 46 - dao.getByCategories(categoryIds) 47 - .map(mapper::map) 48 - } 68 + override suspend fun removeByType(typeId: Long) = mutex.withLockedCache( 69 + logMessage = "removeByType", 70 + accessSource = { dao.deleteByType(typeId) }, 71 + afterSourceAccess = { cache = cache?.removeIf { it.isType() && it.idData.value == typeId } }, 72 + ) 49 73 50 - override suspend fun add(recordTypeGoal: RecordTypeGoal): Long = withContext(Dispatchers.IO) { 51 - Timber.d("add") 52 - return@withContext dao.insert( 53 - recordTypeGoal.let(mapper::map), 54 - ) 55 - } 74 + override suspend fun removeByCategory(categoryId: Long) = mutex.withLockedCache( 75 + logMessage = "removeByCategory", 76 + accessSource = { dao.deleteByCategory(categoryId) }, 77 + afterSourceAccess = { cache = cache?.removeIf { it.isCategory() && it.idData.value == categoryId } }, 78 + ) 56 79 57 - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { 58 - Timber.d("remove") 59 - dao.delete(id) 60 - } 80 + override suspend fun clear() = mutex.withLockedCache( 81 + logMessage = "clear", 82 + accessSource = { dao.clear() }, 83 + afterSourceAccess = { cache = null }, 84 + ) 61 85 62 - override suspend fun removeByType(typeId: Long) = withContext(Dispatchers.IO) { 63 - Timber.d("removeByType") 64 - dao.deleteByType(typeId) 86 + private suspend fun initializeCache() { 87 + cache = dao.getAll().map(mapper::map) 65 88 } 66 89 67 - override suspend fun removeByCategory(categoryId: Long) = withContext(Dispatchers.IO) { 68 - Timber.d("removeByCategory") 69 - dao.deleteByCategory(categoryId) 90 + private fun RecordTypeGoal.isType(): Boolean { 91 + return idData is IdData.Type 70 92 } 71 93 72 - override suspend fun clear() = withContext(Dispatchers.IO) { 73 - Timber.d("clear") 74 - dao.clear() 94 + private fun RecordTypeGoal.isCategory(): Boolean { 95 + return idData is IdData.Category 75 96 } 76 97 }
+48 -38
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RecordTypeRepoImpl.kt
··· 1 1 package com.example.util.simpletimetracker.data_local.repo 2 2 3 3 import com.example.util.simpletimetracker.data_local.database.RecordTypeDao 4 + import com.example.util.simpletimetracker.data_local.utils.withLockedCache 4 5 import com.example.util.simpletimetracker.data_local.mapper.RecordTypeDataLocalMapper 6 + import com.example.util.simpletimetracker.data_local.utils.removeIf 5 7 import com.example.util.simpletimetracker.domain.model.RecordType 6 8 import com.example.util.simpletimetracker.domain.repo.RecordTypeRepo 7 - import kotlinx.coroutines.Dispatchers 8 - import kotlinx.coroutines.withContext 9 - import timber.log.Timber 9 + import kotlinx.coroutines.sync.Mutex 10 10 import javax.inject.Inject 11 11 import javax.inject.Singleton 12 12 13 13 @Singleton 14 14 class RecordTypeRepoImpl @Inject constructor( 15 15 private val recordTypeDao: RecordTypeDao, 16 - private val recordTypeDataLocalMapper: RecordTypeDataLocalMapper 16 + private val recordTypeDataLocalMapper: RecordTypeDataLocalMapper, 17 17 ) : RecordTypeRepo { 18 18 19 - override suspend fun getAll(): List<RecordType> = withContext(Dispatchers.IO) { 20 - Timber.d("getAll") 21 - recordTypeDao.getAll().map(recordTypeDataLocalMapper::map) 22 - } 19 + private var cache: List<RecordType>? = null 20 + private val mutex: Mutex = Mutex() 23 21 24 - override suspend fun get(id: Long): RecordType? = withContext(Dispatchers.IO) { 25 - Timber.d("get id") 26 - recordTypeDao.get(id)?.let(recordTypeDataLocalMapper::map) 27 - } 22 + override suspend fun getAll(): List<RecordType> = mutex.withLockedCache( 23 + logMessage = "getAll", 24 + accessCache = { cache }, 25 + accessSource = { recordTypeDao.getAll().map(recordTypeDataLocalMapper::map) }, 26 + afterSourceAccess = { cache = it }, 27 + ) 28 28 29 - override suspend fun get(name: String): RecordType? = withContext(Dispatchers.IO) { 30 - Timber.d("get name") 31 - recordTypeDao.get(name)?.let(recordTypeDataLocalMapper::map) 32 - } 29 + override suspend fun get(id: Long): RecordType? = mutex.withLockedCache( 30 + logMessage = "get id", 31 + accessCache = { cache?.firstOrNull { it.id == id } }, 32 + accessSource = { recordTypeDao.get(id)?.let(recordTypeDataLocalMapper::map) }, 33 + ) 33 34 34 - override suspend fun add(recordType: RecordType): Long = withContext(Dispatchers.IO) { 35 - Timber.d("add") 36 - return@withContext recordTypeDao.insert( 37 - recordType.let(recordTypeDataLocalMapper::map) 38 - ) 39 - } 35 + override suspend fun get(name: String): RecordType? = mutex.withLockedCache( 36 + logMessage = "get name", 37 + accessCache = { cache?.firstOrNull { it.name == name } }, 38 + accessSource = { recordTypeDao.get(name)?.let(recordTypeDataLocalMapper::map) }, 39 + ) 40 40 41 - override suspend fun archive(id: Long) = withContext(Dispatchers.IO) { 42 - Timber.d("archive") 43 - recordTypeDao.archive(id) 44 - } 41 + override suspend fun add(recordType: RecordType): Long = mutex.withLockedCache( 42 + logMessage = "add", 43 + accessSource = { recordTypeDao.insert(recordType.let(recordTypeDataLocalMapper::map)) }, 44 + afterSourceAccess = { cache = null }, 45 + ) 45 46 46 - override suspend fun restore(id: Long) = withContext(Dispatchers.IO) { 47 - Timber.d("restore") 48 - recordTypeDao.restore(id) 49 - } 47 + override suspend fun archive(id: Long) = mutex.withLockedCache( 48 + logMessage = "archive", 49 + accessSource = { recordTypeDao.archive(id) }, 50 + afterSourceAccess = { cache = cache?.map { if (it.id == id) it.copy(hidden = true) else it } }, 51 + ) 52 + 53 + override suspend fun restore(id: Long) = mutex.withLockedCache( 54 + logMessage = "restore", 55 + accessSource = { recordTypeDao.restore(id) }, 56 + afterSourceAccess = { cache = cache?.map { if (it.id == id) it.copy(hidden = false) else it } }, 57 + ) 50 58 51 - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { 52 - Timber.d("remove") 53 - recordTypeDao.delete(id) 54 - } 59 + override suspend fun remove(id: Long) = mutex.withLockedCache( 60 + logMessage = "remove", 61 + accessSource = { recordTypeDao.delete(id) }, 62 + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, 63 + ) 55 64 56 - override suspend fun clear() = withContext(Dispatchers.IO) { 57 - Timber.d("clear") 58 - recordTypeDao.clear() 59 - } 65 + override suspend fun clear() = mutex.withLockedCache( 66 + logMessage = "clear", 67 + accessSource = { recordTypeDao.clear() }, 68 + afterSourceAccess = { cache = null }, 69 + ) 60 70 }
+6
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RepoConstants.kt
··· 1 + package com.example.util.simpletimetracker.data_local.repo 2 + 3 + object RepoConstants { 4 + const val LOG_MESSAGE_PREFIX = "DB_ACCESS" 5 + const val LOG_MESSAGE_CACHE = "CACHE" 6 + }
+38 -30
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RunningRecordRepoImpl.kt
··· 2 2 3 3 import com.example.util.simpletimetracker.data_local.database.RunningRecordDao 4 4 import com.example.util.simpletimetracker.data_local.mapper.RunningRecordDataLocalMapper 5 + import com.example.util.simpletimetracker.data_local.utils.removeIf 6 + import com.example.util.simpletimetracker.data_local.utils.withLockedCache 5 7 import com.example.util.simpletimetracker.domain.model.RunningRecord 6 8 import com.example.util.simpletimetracker.domain.repo.RunningRecordRepo 7 - import kotlinx.coroutines.Dispatchers 8 - import kotlinx.coroutines.withContext 9 - import timber.log.Timber 9 + import kotlinx.coroutines.sync.Mutex 10 10 import javax.inject.Inject 11 11 import javax.inject.Singleton 12 12 13 13 @Singleton 14 14 class RunningRecordRepoImpl @Inject constructor( 15 15 private val runningRecordDao: RunningRecordDao, 16 - private val runningRunningRecordLocalMapper: RunningRecordDataLocalMapper 16 + private val runningRunningRecordLocalMapper: RunningRecordDataLocalMapper, 17 17 ) : RunningRecordRepo { 18 18 19 - override suspend fun isEmpty(): Boolean = withContext(Dispatchers.IO) { 20 - Timber.d("isEmpty") 21 - return@withContext runningRecordDao.isEmpty() == 0L 22 - } 19 + private var cache: List<RunningRecord>? = null 20 + private val mutex: Mutex = Mutex() 23 21 24 - override suspend fun getAll(): List<RunningRecord> = withContext(Dispatchers.IO) { 25 - Timber.d("getAll") 26 - runningRecordDao.getAll().map(runningRunningRecordLocalMapper::map) 27 - } 22 + override suspend fun isEmpty(): Boolean = mutex.withLockedCache( 23 + logMessage = "isEmpty", 24 + accessCache = { cache?.isEmpty() }, 25 + accessSource = { runningRecordDao.isEmpty() == 0L }, 26 + ) 28 27 29 - override suspend fun get(id: Long): RunningRecord? = withContext(Dispatchers.IO) { 30 - Timber.d("get") 31 - runningRecordDao.get(id)?.let(runningRunningRecordLocalMapper::map) 32 - } 28 + override suspend fun getAll(): List<RunningRecord> = mutex.withLockedCache( 29 + logMessage = "getAll", 30 + accessCache = { cache }, 31 + accessSource = { runningRecordDao.getAll().map(runningRunningRecordLocalMapper::map) }, 32 + afterSourceAccess = { cache = it }, 33 + ) 33 34 34 - override suspend fun add(runningRecord: RunningRecord): Long = withContext(Dispatchers.IO) { 35 - Timber.d("add") 36 - return@withContext runningRecordDao.insert( 37 - runningRecord.let(runningRunningRecordLocalMapper::map) 38 - ) 39 - } 35 + override suspend fun get(id: Long): RunningRecord? = mutex.withLockedCache( 36 + logMessage = "get", 37 + accessCache = { cache?.firstOrNull { it.id == id } }, 38 + accessSource = { runningRecordDao.get(id)?.let(runningRunningRecordLocalMapper::map) }, 39 + ) 40 40 41 - override suspend fun remove(id: Long) = withContext(Dispatchers.IO) { 42 - Timber.d("remove") 43 - runningRecordDao.delete(id) 44 - } 41 + override suspend fun add(runningRecord: RunningRecord): Long = mutex.withLockedCache( 42 + logMessage = "add", 43 + accessSource = { runningRecordDao.insert(runningRecord.let(runningRunningRecordLocalMapper::map)) }, 44 + afterSourceAccess = { cache = null }, 45 + ) 45 46 46 - override suspend fun clear() = withContext(Dispatchers.IO) { 47 - Timber.d("clear") 48 - runningRecordDao.clear() 49 - } 47 + override suspend fun remove(id: Long) = mutex.withLockedCache( 48 + logMessage = "remove", 49 + accessSource = { runningRecordDao.delete(id) }, 50 + afterSourceAccess = { cache = cache?.removeIf { it.id == id } }, 51 + ) 52 + 53 + override suspend fun clear() = mutex.withLockedCache( 54 + logMessage = "clear", 55 + accessSource = { runningRecordDao.clear() }, 56 + afterSourceAccess = { cache = null }, 57 + ) 50 58 }
+5 -5
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/RunningRecordToRecordTagRepoImpl.kt
··· 2 2 3 3 import com.example.util.simpletimetracker.data_local.database.RunningRecordToRecordTagDao 4 4 import com.example.util.simpletimetracker.data_local.mapper.RunningRecordToRecordTagDataLocalMapper 5 + import com.example.util.simpletimetracker.data_local.utils.logDataAccess 5 6 import com.example.util.simpletimetracker.domain.repo.RunningRecordToRecordTagRepo 6 7 import kotlinx.coroutines.Dispatchers 7 8 import kotlinx.coroutines.withContext 8 - import timber.log.Timber 9 9 import javax.inject.Inject 10 10 import javax.inject.Singleton 11 11 ··· 17 17 18 18 override suspend fun addRunningRecordTags(runningRecordId: Long, tagIds: List<Long>) = 19 19 withContext(Dispatchers.IO) { 20 - Timber.d("add running record tags") 20 + logDataAccess("add running record tags") 21 21 tagIds.map { 22 22 mapper.map(recordId = runningRecordId, recordTagId = it) 23 23 }.let { ··· 27 27 28 28 override suspend fun removeAllByTagId(tagId: Long) = 29 29 withContext(Dispatchers.IO) { 30 - Timber.d("remove all by tagId") 30 + logDataAccess("remove all by tagId") 31 31 dao.deleteAllByTagId(tagId) 32 32 } 33 33 34 34 override suspend fun removeAllByRunningRecordId(runningRecordId: Long) = 35 35 withContext(Dispatchers.IO) { 36 - Timber.d("remove all by runningRecordId") 36 + logDataAccess("remove all by runningRecordId") 37 37 dao.deleteAllByRecordId(runningRecordId) 38 38 } 39 39 40 40 override suspend fun clear() = 41 41 withContext(Dispatchers.IO) { 42 - Timber.d("clear") 42 + logDataAccess("clear") 43 43 dao.clear() 44 44 } 45 45 }
+91
data_local/src/main/java/com/example/util/simpletimetracker/data_local/utils/PrefsUtils.kt
··· 1 + package com.example.util.simpletimetracker.data_local.utils 2 + 3 + import android.content.SharedPreferences 4 + import com.example.util.simpletimetracker.data_local.repo.RepoConstants.LOG_MESSAGE_CACHE 5 + import com.example.util.simpletimetracker.data_local.repo.RepoConstants.LOG_MESSAGE_PREFIX 6 + import kotlinx.coroutines.Dispatchers 7 + import kotlinx.coroutines.sync.Mutex 8 + import kotlinx.coroutines.sync.withLock 9 + import kotlinx.coroutines.withContext 10 + import timber.log.Timber 11 + import kotlin.properties.ReadWriteProperty 12 + import kotlin.reflect.KProperty 13 + 14 + @Suppress("UNCHECKED_CAST") 15 + inline fun <reified T> SharedPreferences.delegate( 16 + key: String, 17 + default: T, 18 + ) = object : ReadWriteProperty<Any?, T> { 19 + override fun getValue(thisRef: Any?, property: KProperty<*>): T = 20 + when (default) { 21 + is Boolean -> (getBoolean(key, default) as? T) ?: default 22 + is Int -> (getInt(key, default) as? T) ?: default 23 + is Long -> (getLong(key, default) as? T) ?: default 24 + is String -> (getString(key, default) as? T) ?: default 25 + is Set<*> -> (getStringSet(key, default as? Set<String>)?.toSet() as? T) ?: default 26 + else -> throw IllegalArgumentException( 27 + "Prefs delegate not implemented for class ${(default as Any?)?.javaClass}", 28 + ) 29 + } 30 + 31 + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = with(edit()) { 32 + when (value) { 33 + is Boolean -> putBoolean(key, value) 34 + is Int -> putInt(key, value) 35 + is Long -> putLong(key, value) 36 + is String -> putString(key, value) 37 + is Set<*> -> putStringSet(key, value as? Set<String>) 38 + else -> throw IllegalArgumentException( 39 + "Prefs delegate not implemented for class ${(default as Any?)?.javaClass}", 40 + ) 41 + } 42 + apply() 43 + } 44 + } 45 + 46 + internal suspend inline fun <T> Mutex.withLockedCache( 47 + logMessage: String, 48 + crossinline accessCache: () -> T? = { null }, 49 + crossinline accessSource: suspend () -> T, 50 + ): T { 51 + return withLockedCache( 52 + logMessage = logMessage, 53 + accessCache = accessCache, 54 + accessSource = accessSource, 55 + afterSourceAccess = {}, 56 + ) 57 + } 58 + 59 + internal suspend inline fun <T> Mutex.withLockedCache( 60 + logMessage: String, 61 + crossinline accessCache: () -> T? = { null }, 62 + crossinline accessSource: suspend () -> T, 63 + crossinline afterSourceAccess: suspend (T) -> Unit, 64 + ): T { 65 + return withContext(Dispatchers.IO) { 66 + this@withLockedCache.withLock { 67 + accessCache()?.let { 68 + logDataAccess("$logMessage ($LOG_MESSAGE_CACHE)") 69 + it 70 + } ?: run { 71 + logDataAccess(logMessage) 72 + accessSource().also { afterSourceAccess(it) } 73 + } 74 + } 75 + } 76 + } 77 + 78 + /** 79 + * Inlined for message tag to be an actual class name at call site. 80 + */ 81 + @Suppress("NOTHING_TO_INLINE") 82 + internal inline fun logDataAccess(logMessage: String) { 83 + Timber.d("$LOG_MESSAGE_PREFIX $logMessage") 84 + } 85 + 86 + /** 87 + * Produces a new list from original list by removing elements satisfying filter block. 88 + */ 89 + inline fun <T> List<T>.removeIf(crossinline filter: (T) -> Boolean): List<T> { 90 + return this.toMutableList().apply { removeIf { filter(it) } } 91 + }
+2 -2
domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeCategoryInteractor.kt
··· 12 12 return recordTypeCategoryRepo.getAll() 13 13 } 14 14 15 - suspend fun getCategories(typeId: Long): List<Long> { 15 + suspend fun getCategories(typeId: Long): Set<Long> { 16 16 return recordTypeCategoryRepo.getCategoryIdsByType(typeId) 17 17 } 18 18 ··· 24 24 recordTypeCategoryRepo.removeCategories(typeId, categoryIds) 25 25 } 26 26 27 - suspend fun getTypes(categoryId: Long): List<Long> { 27 + suspend fun getTypes(categoryId: Long): Set<Long> { 28 28 return recordTypeCategoryRepo.getTypeIdsByCategory(categoryId) 29 29 } 30 30
-4
domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeGoalInteractor.kt
··· 28 28 return repo.getByCategory(categoryId) 29 29 } 30 30 31 - suspend fun getByCategories(categoryIds: List<Long>): List<RecordTypeGoal> { 32 - return repo.getByCategories(categoryIds) 33 - } 34 - 35 31 suspend fun add(recordTypeGoal: RecordTypeGoal) { 36 32 repo.add(recordTypeGoal) 37 33 }
+2 -2
domain/src/main/java/com/example/util/simpletimetracker/domain/repo/RecordTypeCategoryRepo.kt
··· 9 9 10 10 suspend fun getCategoriesByType(typeId: Long): List<Category> 11 11 12 - suspend fun getCategoryIdsByType(typeId: Long): List<Long> 12 + suspend fun getCategoryIdsByType(typeId: Long): Set<Long> 13 13 14 - suspend fun getTypeIdsByCategory(categoryId: Long): List<Long> 14 + suspend fun getTypeIdsByCategory(categoryId: Long): Set<Long> 15 15 16 16 suspend fun add(recordTypeCategory: RecordTypeCategory) 17 17
-2
domain/src/main/java/com/example/util/simpletimetracker/domain/repo/RecordTypeGoalRepo.kt
··· 14 14 15 15 suspend fun getByCategory(categoryId: Long): List<RecordTypeGoal> 16 16 17 - suspend fun getByCategories(categoryIds: List<Long>): List<RecordTypeGoal> 18 - 19 17 suspend fun add(recordTypeGoal: RecordTypeGoal): Long 20 18 21 19 suspend fun remove(id: Long)
+1 -1
features/feature_change_category/src/main/java/com/example/util/simpletimetracker/feature_change_category/viewModel/ChangeCategoryViewModel.kt
··· 88 88 val keyboardVisibility: LiveData<Boolean> by lazy { MutableLiveData(categoryId == 0L) } 89 89 90 90 private val categoryId: Long get() = (extra as? ChangeTagData.Change)?.id.orZero() 91 - private var initialTypes: List<Long> = emptyList() 91 + private var initialTypes: Set<Long> = emptySet() 92 92 private var newName: String = "" 93 93 private var newColor: AppColor = AppColor(colorId = (0..ColorMapper.colorsNumber).random(), colorInt = "") 94 94 private var newTypes: MutableList<Long> = mutableListOf()
+1 -1
features/feature_change_record_type/src/main/java/com/example/util/simpletimetracker/feature_change_record_type/viewModel/ChangeRecordTypeViewModel.kt
··· 128 128 129 129 private val recordTypeId: Long get() = (extra as? ChangeRecordTypeParams.Change)?.id.orZero() 130 130 private var iconType: IconType = IconType.IMAGE 131 - private var initialCategories: List<Long> = emptyList() 131 + private var initialCategories: Set<Long> = emptySet() 132 132 private var newName: String = "" 133 133 private var newIconName: String = "" 134 134 private var newCategories: MutableList<Long> = mutableListOf()