Refactor favorites and searchable database

This commit is contained in:
MM20
2023-04-10 22:44:05 +02:00
parent 582c94b6af
commit ed70097c38
49 changed files with 1238 additions and 501 deletions
+1 -1
View File
@@ -44,5 +44,5 @@ dependencies {
implementation(project(":core:base"))
implementation(project(":core:ktx"))
implementation(project(":core:crashreporter"))
implementation(project(":data:favorites"))
implementation(project(":data:searchable"))
}
@@ -3,7 +3,7 @@ package de.mm20.launcher2.data.customattrs
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SavableSearchable
import kotlinx.collections.immutable.ImmutableList
@@ -46,7 +46,7 @@ interface CustomAttributesRepository {
internal class CustomAttributesRepositoryImpl(
private val appDatabase: AppDatabase,
private val favoritesRepository: FavoritesRepository
private val searchableRepository: SearchableRepository
) : CustomAttributesRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
@@ -79,7 +79,7 @@ internal class CustomAttributesRepositoryImpl(
override fun setCustomLabel(searchable: SavableSearchable, label: String) {
val dao = appDatabase.customAttrsDao()
scope.launch {
favoritesRepository.save(searchable)
searchableRepository.insert(searchable)
appDatabase.runInTransaction {
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
dao.setCustomAttribute(
@@ -102,7 +102,7 @@ internal class CustomAttributesRepositoryImpl(
override fun setTags(searchable: SavableSearchable, tags: List<String>) {
val dao = appDatabase.customAttrsDao()
scope.launch {
favoritesRepository.save(searchable)
searchableRepository.insert(searchable)
dao.setTags(searchable.key, tags.map {
CustomTag(it).toDatabaseEntity(searchable.key)
})
@@ -128,7 +128,7 @@ internal class CustomAttributesRepositoryImpl(
override fun getItemsForTag(tag: String): Flow<List<SavableSearchable>> {
val dao = appDatabase.customAttrsDao()
return dao.getItemsWithTag(tag).map {
favoritesRepository.getFromKeys(it)
searchableRepository.getByKeys(it)
}
}
@@ -137,7 +137,7 @@ internal class CustomAttributesRepositoryImpl(
return scope.launch {
dao.setItemsWithTag(tag, items.map { it.key })
for (item in items) {
favoritesRepository.save(item)
searchableRepository.insert(item)
}
}
}
@@ -172,7 +172,7 @@ internal class CustomAttributesRepositoryImpl(
}
val dao = appDatabase.customAttrsDao()
return dao.search("%$query%").map {
favoritesRepository.getFromKeys(it).toImmutableList()
searchableRepository.getByKeys(it).toImmutableList()
}
}
@@ -1,8 +0,0 @@
package de.mm20.launcher2.favorites
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val favoritesModule = module {
single<FavoritesRepository> { FavoritesRepositoryImpl(androidContext(), get(), get()) }
}
@@ -0,0 +1,8 @@
package de.mm20.launcher2.searchable
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val searchableModule = module {
single<SearchableRepository> { SearchableRepositoryImpl(androidContext(), get(), get()) }
}
@@ -1,4 +1,4 @@
package de.mm20.launcher2.favorites
package de.mm20.launcher2.searchable
import de.mm20.launcher2.database.entities.SavedSearchableEntity
import de.mm20.launcher2.search.SavableSearchable
@@ -1,4 +1,4 @@
package de.mm20.launcher2.favorites
package de.mm20.launcher2.searchable
data class SavedSearchableRankInfo(
val key: String,
@@ -1,58 +1,87 @@
package de.mm20.launcher2.favorites
package de.mm20.launcher2.searchable
import android.content.Context
import android.util.Log
import androidx.room.withTransaction
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.SavedSearchableEntity
import de.mm20.launcher2.database.entities.SavedSearchableUpdatePinEntity
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONException
import org.koin.core.component.KoinComponent
import java.io.File
interface FavoritesRepository {
interface SearchableRepository {
fun insert(
searchable: SavableSearchable,
)
fun upsert(
searchable: SavableSearchable,
hidden: Boolean? = null,
pinned: Boolean? = null,
launchCount: Int? = null,
weight: Double? = null,
)
fun update(
searchable: SavableSearchable,
hidden: Boolean? = null,
pinned: Boolean? = null,
launchCount: Int? = null,
weight: Double? = null,
)
/**
* Get favorites
* @param includeTypes Include only items of these types. Cannot be used together with excludeTypes.
* @param excludeTypes Exclude only items of these types. Cannot be used together with includeTypes.
* @param manuallySorted Include items that have been sorted manually
* @param automaticallySorted Include items that are pinned but not sorted
* @param frequentlyUsed Include items that are not pinned but most frequently used
* @param limit Maximum number of items returned.
*/
fun getFavorites(
* Touch a searchable to update its weight and launch counter
**/
fun touch(
searchable: SavableSearchable,
)
fun get(
includeTypes: List<String>? = null,
excludeTypes: List<String>? = null,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
limit: Int = 100
hidden: Boolean = false,
limit: Int = 100,
): Flow<List<SavableSearchable>>
fun getKeys(
includeTypes: List<String>? = null,
excludeTypes: List<String>? = null,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
hidden: Boolean = false,
limit: Int = 100,
): Flow<List<String>>
fun getHiddenCalendarEventKeys(): Flow<List<String>>
fun isPinned(searchable: SavableSearchable): Flow<Boolean>
fun pinItem(searchable: SavableSearchable)
fun unpinItem(searchable: SavableSearchable)
fun isHidden(searchable: SavableSearchable): Flow<Boolean>
fun hideItem(searchable: SavableSearchable)
fun unhideItem(searchable: SavableSearchable)
fun incrementLaunchCounter(searchable: SavableSearchable)
fun updateFavorites(
manuallySorted: List<SavableSearchable>,
automaticallySorted: List<SavableSearchable>,
)
fun getHiddenItems(): Flow<List<SavableSearchable>>
fun getHiddenItemKeys(): Flow<List<String>>
/**
* Returns the given keys sorted by relevance.
* The first item in the list is the most relevant.
@@ -65,24 +94,13 @@ interface FavoritesRepository {
/**
* Remove this item from the Searchable database
*/
fun remove(searchable: SavableSearchable)
/**
* Remove this item from favorites and reset launch counter
*/
fun removeFromFavorites(searchable: SavableSearchable)
/**
* Ensure that this searchable exists in the Favorites table.
* If it doesn't exist, insert it with 0 launch count, not pinned and not hidden
*/
fun save(searchable: SavableSearchable)
fun delete(searchable: SavableSearchable)
/**
* Get items with the given keys from the favorites database.
* Items that don't exist in the database will not be returned.
*/
suspend fun getFromKeys(keys: List<String>): List<SavableSearchable>
suspend fun getByKeys(keys: List<String>): List<SavableSearchable>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
@@ -95,177 +113,193 @@ interface FavoritesRepository {
suspend fun cleanupDatabase(): Int
}
internal class FavoritesRepositoryImpl(
internal class SearchableRepositoryImpl(
private val context: Context,
private val database: AppDatabase,
private val dataStore: LauncherDataStore
) : FavoritesRepository, KoinComponent {
) : SearchableRepository, KoinComponent {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
override fun getFavorites(
override fun insert(searchable: SavableSearchable) {
val dao = database.searchableDao()
scope.launch {
dao.insert(
SavedSearchableEntity(
key = searchable.key,
type = searchable.domain,
serializedSearchable = searchable.serialize() ?: return@launch,
hidden = false,
launchCount = 0,
weight = 0.0,
pinPosition = 0,
)
)
}
}
override fun upsert(
searchable: SavableSearchable,
hidden: Boolean?,
pinned: Boolean?,
launchCount: Int?,
weight: Double?
) {
val dao = database.searchableDao()
scope.launch {
val entity = dao.getByKey(searchable.key).firstOrNull()
dao.upsert(
SavedSearchableEntity(
key = searchable.key,
type = searchable.domain,
hidden = hidden ?: entity?.hidden ?: false,
pinPosition = pinned?.let { if (it) 1 else 0 } ?: entity?.pinPosition ?: 0,
launchCount = launchCount ?: entity?.launchCount ?: 0,
weight = weight ?: entity?.weight ?: 0.0,
serializedSearchable = searchable.serialize() ?: return@launch,
)
)
}
}
override fun update(
searchable: SavableSearchable,
hidden: Boolean?,
pinned: Boolean?,
launchCount: Int?,
weight: Double?
) {
val dao = database.searchableDao()
scope.launch {
val entity = dao.getByKey(searchable.key).firstOrNull()
dao.upsert(
SavedSearchableEntity(
key = searchable.key,
type = searchable.domain,
hidden = hidden ?: entity?.hidden ?: false,
pinPosition = pinned?.let { if (it) 1 else 0 } ?: entity?.pinPosition ?: 0,
launchCount = launchCount ?: entity?.launchCount ?: 0,
weight = weight ?: entity?.weight ?: 0.0,
serializedSearchable = searchable.serialize() ?: return@launch,
)
)
}
}
override fun touch(searchable: SavableSearchable) {
scope.launch {
val weightFactor =
when (dataStore.data.map { it.resultOrdering.weightFactor }.firstOrNull()) {
WeightFactor.Low -> WEIGHT_FACTOR_LOW
WeightFactor.High -> WEIGHT_FACTOR_HIGH
else -> WEIGHT_FACTOR_MEDIUM
}
val item = SavedSearchable(searchable.key, searchable, 0, 0, false, 0.0)
item.toDatabaseEntity()?.let {
database.searchableDao()
.touch(it, weightFactor)
}
}
}
override fun get(
includeTypes: List<String>?,
excludeTypes: List<String>?,
manuallySorted: Boolean,
automaticallySorted: Boolean,
frequentlyUsed: Boolean,
hidden: Boolean,
limit: Int
): Flow<List<SavableSearchable>> {
val dao = database.searchDao()
val dao = database.searchableDao()
val entities = when {
includeTypes == null && excludeTypes == null -> dao.getFavorites(
includeTypes == null && excludeTypes == null -> dao.get(
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
includeTypes != null && excludeTypes == null -> {
dao.getFavoritesWithTypes(
includeTypes = includeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
limit = limit
)
}
includeTypes == null -> dao.getExcludeTypes(
excludeTypes = excludeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
excludeTypes != null && includeTypes == null -> {
dao.getFavoritesWithoutTypes(
excludeTypes = excludeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
limit = limit
)
}
excludeTypes == null -> dao.getIncludeTypes(
includeTypes = includeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
else -> throw IllegalArgumentException("You can either use includeTypes or excludeTypes, not both")
else -> throw IllegalArgumentException("Cannot specify both includeTypes and excludeTypes")
}
return entities.map {
it.mapNotNull { fromDatabaseEntity(it).searchable }
}
}
override fun getHiddenCalendarEventKeys(): Flow<List<String>> {
return database.searchDao().getHiddenCalendarEventKeys()
override fun getKeys(
includeTypes: List<String>?,
excludeTypes: List<String>?,
manuallySorted: Boolean,
automaticallySorted: Boolean,
frequentlyUsed: Boolean,
hidden: Boolean,
limit: Int
): Flow<List<String>> {
val dao = database.searchableDao()
return when {
includeTypes == null && excludeTypes == null -> dao.getKeys(
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
includeTypes == null -> dao.getKeysExcludeTypes(
excludeTypes = excludeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
excludeTypes == null -> dao.getKeysIncludeTypes(
includeTypes = includeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
else -> throw IllegalArgumentException("Cannot specify both includeTypes and excludeTypes")
}
}
override fun isPinned(searchable: SavableSearchable): Flow<Boolean> {
return database.searchDao().isPinned(searchable.key)
}
override fun pinItem(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val dao = database.searchDao()
val databaseItem = dao.getFavorite(searchable.key)
val savedSearchable = SavedSearchable(
key = searchable.key,
searchable = searchable,
launchCount = databaseItem?.launchCount ?: 0,
pinPosition = 1,
hidden = false,
weight = databaseItem?.weight ?: 0.0
)
savedSearchable.toDatabaseEntity()?.let { dao.insertReplaceExisting(it) }
}
}
}
override fun unpinItem(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
database.searchDao().unpinFavorite(searchable.key)
}
}
return database.searchableDao().isPinned(searchable.key)
}
override fun isHidden(searchable: SavableSearchable): Flow<Boolean> {
return database.searchDao().isHidden(searchable.key)
return database.searchableDao().isHidden(searchable.key)
}
override fun hideItem(searchable: SavableSearchable) {
override fun delete(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val dao = database.searchDao()
val databaseItem = dao.getFavorite(searchable.key)
val savedSearchable = SavedSearchable(
key = searchable.key,
searchable = searchable,
launchCount = databaseItem?.launchCount ?: 0,
pinPosition = 0,
hidden = true,
weight = databaseItem?.weight ?: 0.0
)
savedSearchable.toDatabaseEntity()?.let { dao.insertReplaceExisting(it) }
}
}
}
override fun unhideItem(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
database.searchDao().unhideItem(searchable.key)
}
}
}
override fun incrementLaunchCounter(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val weightFactor =
when (dataStore.data.map { it.resultOrdering.weightFactor }.firstOrNull()) {
WeightFactor.Low -> WEIGHT_FACTOR_LOW
WeightFactor.High -> WEIGHT_FACTOR_HIGH
else -> WEIGHT_FACTOR_MEDIUM
}
val item = SavedSearchable(searchable.key, searchable, 0, 0, false, 0.0)
item.toDatabaseEntity()?.let {
database.searchDao()
.incrementLaunchCount(it, weightFactor)
}
}
}
}
override fun getHiddenItems(): Flow<List<SavableSearchable>> {
return database.searchDao().getHiddenItems().map {
it.mapNotNull { fromDatabaseEntity(it).searchable }
}
}
override fun getHiddenItemKeys(): Flow<List<String>> {
return database.searchDao().getHiddenItemKeys()
}
override fun remove(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
database.searchDao().deleteByKey(searchable.key)
}
}
}
override fun removeFromFavorites(searchable: SavableSearchable) {
scope.launch {
database.searchDao().resetPinStatusAndLaunchCounter(searchable.key)
}
}
override fun save(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val entity = SavedSearchable(
key = searchable.key,
searchable = searchable,
launchCount = 0,
pinPosition = 0,
hidden = false,
weight = 0.0
).toDatabaseEntity() ?: return@withContext
database.searchDao().insertSkipExisting(entity)
}
database.searchableDao().delete(searchable.key)
}
}
@@ -273,51 +307,42 @@ internal class FavoritesRepositoryImpl(
manuallySorted: List<SavableSearchable>,
automaticallySorted: List<SavableSearchable>
) {
val dao = database.searchDao()
val dao = database.searchableDao()
scope.launch {
withContext(Dispatchers.IO) {
val keys = manuallySorted.map { it.key } + automaticallySorted.map { it.key }
val entities = dao.getFromKeys(keys)
val updatedManuallySorted = manuallySorted.mapIndexedNotNull { index, searchable ->
val entity = entities.find { searchable.key == it.key } ?: SavedSearchable(
key = searchable.key,
searchable = searchable,
launchCount = 0,
pinPosition = 0,
hidden = false,
weight = 0.0
).toDatabaseEntity() ?: return@mapIndexedNotNull null
entity.pinPosition = manuallySorted.size - index + 1
entity
}
val updatedAutomaticallySorted =
automaticallySorted.mapIndexedNotNull { index, searchable ->
val entity = entities.find { searchable.key == it.key } ?: SavedSearchable(
key = searchable.key,
searchable = searchable,
launchCount = 0,
pinPosition = 0,
hidden = false,
weight = 0.0
).toDatabaseEntity() ?: return@mapIndexedNotNull null
entity.pinPosition = 1
entity
database.withTransaction {
dao.unpinAll()
dao.upsert(
manuallySorted.mapIndexedNotNull { index, savableSearchable ->
SavedSearchableUpdatePinEntity(
key = savableSearchable.key,
type = savableSearchable.domain,
pinPosition = manuallySorted.size - index + 1,
serializedSearchable = savableSearchable.serialize()
?: return@mapIndexedNotNull null,
)
}
database.runInTransaction {
dao.unpinAll()
dao.insertAllReplaceExisting(updatedManuallySorted)
dao.insertAllReplaceExisting(updatedAutomaticallySorted)
}
)
dao.upsert(
automaticallySorted.mapNotNull { savableSearchable ->
SavedSearchableUpdatePinEntity(
key = savableSearchable.key,
type = savableSearchable.domain,
pinPosition = 1,
serializedSearchable = savableSearchable.serialize()
?: return@mapNotNull null,
)
}
)
}
}
}
override fun sortByRelevance(keys: List<String>): Flow<List<String>> {
return database.searchDao().sortByRelevance(keys)
return database.searchableDao().sortByRelevance(keys)
}
override fun sortByWeight(keys: List<String>): Flow<List<String>> {
return database.searchDao().sortByWeight(keys)
return database.searchableDao().sortByWeight(keys)
}
private fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable {
@@ -337,13 +362,13 @@ internal class FavoritesRepositoryImpl(
private fun removeInvalidItem(key: String) {
scope.launch {
database.searchDao().deleteByKey(key)
database.searchableDao().delete(key)
}
}
override suspend fun getFromKeys(keys: List<String>): List<SavableSearchable> {
val dao = database.searchDao()
return dao.getFromKeys(keys)
override suspend fun getByKeys(keys: List<String>): List<SavableSearchable> {
val dao = database.searchableDao()
return dao.getByKeys(keys)
.mapNotNull { fromDatabaseEntity(it).searchable }
}
@@ -1,4 +1,4 @@
package de.mm20.launcher2.favorites
package de.mm20.launcher2.searchable
import android.content.Context
import de.mm20.launcher2.appshortcuts.LauncherShortcutDeserializer
@@ -12,6 +12,7 @@ import de.mm20.launcher2.contacts.ContactSerializer
import de.mm20.launcher2.files.*
import de.mm20.launcher2.search.NullDeserializer
import de.mm20.launcher2.search.NullSerializer
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
@@ -21,6 +22,10 @@ import de.mm20.launcher2.websites.WebsiteSerializer
import de.mm20.launcher2.wikipedia.WikipediaDeserializer
import de.mm20.launcher2.wikipedia.WikipediaSerializer
internal fun SavableSearchable.serialize(): String? {
val serializer = getSerializer(this)
return serializer.serialize(this)
}
internal fun getSerializer(searchable: Searchable?): SearchableSerializer {
if (searchable is LauncherApp) {
@@ -1,4 +1,4 @@
package de.mm20.launcher2.favorites
package de.mm20.launcher2.searchable
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer