diff --git a/customattrs/build.gradle.kts b/customattrs/build.gradle.kts index e4d18144..25da3f2f 100644 --- a/customattrs/build.gradle.kts +++ b/customattrs/build.gradle.kts @@ -44,5 +44,5 @@ dependencies { implementation(project(":search")) implementation(project(":ktx")) implementation(project(":crashreporter")) - + implementation(project(":favorites")) } \ No newline at end of file diff --git a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt index 56fdcb7e..7ee039f3 100644 --- a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt +++ b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt @@ -4,6 +4,7 @@ import android.util.Log 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.ktx.jsonObjectOf import de.mm20.launcher2.search.data.Searchable import kotlinx.coroutines.* @@ -21,12 +22,15 @@ interface CustomAttributesRepository { fun setCustomLabel(searchable: Searchable, label: String) fun clearCustomLabel(searchable: Searchable) + suspend fun search(query: String): Flow> + suspend fun export(toDir: File) suspend fun import(fromDir: File) } internal class CustomAttributesRepositoryImpl( private val appDatabase: AppDatabase, + private val favoritesRepository: FavoritesRepository ) : CustomAttributesRepository { private val scope = CoroutineScope(Job() + Dispatchers.Default) @@ -59,6 +63,7 @@ internal class CustomAttributesRepositoryImpl( override fun setCustomLabel(searchable: Searchable, label: String) { val dao = appDatabase.customAttrsDao() scope.launch { + favoritesRepository.save(searchable) appDatabase.runInTransaction { dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value) dao.setCustomAttribute( @@ -78,6 +83,13 @@ internal class CustomAttributesRepositoryImpl( } } + override suspend fun search(query: String): Flow> { + val dao = appDatabase.customAttrsDao() + return dao.search("%$query%").map { + favoritesRepository.getFromKeys(it) + } + } + override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { val dao = appDatabase.backupDao() var page = 0 diff --git a/customattrs/src/main/java/de/mm20/launcher2/customattrs/Module.kt b/customattrs/src/main/java/de/mm20/launcher2/customattrs/Module.kt index 6088e7db..68aa632f 100644 --- a/customattrs/src/main/java/de/mm20/launcher2/customattrs/Module.kt +++ b/customattrs/src/main/java/de/mm20/launcher2/customattrs/Module.kt @@ -3,5 +3,5 @@ package de.mm20.launcher2.customattrs import org.koin.dsl.module val customAttrsModule = module { - single { CustomAttributesRepositoryImpl(get()) } + single { CustomAttributesRepositoryImpl(get(), get()) } } \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt b/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt index e03f550c..59d4497e 100644 --- a/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt @@ -19,4 +19,7 @@ interface CustomAttrsDao { @Query("SELECT * FROM CustomAttributes WHERE type = :type AND key IN (:keys)") fun getCustomAttributes(keys: List, type: String) : Flow> + + @Query("SELECT DISTINCT key FROM CustomAttributes WHERE (type = 'label' OR type = 'tag') AND value LIKE :query") + fun search(query: String): Flow> } \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt b/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt index ecd562a7..7194e5e7 100644 --- a/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt @@ -106,6 +106,9 @@ interface SearchDao { @Query("SELECT * FROM Searchable WHERE `key` = :key") fun getFavorite(key: String): FavoritesItemEntity? + @Query("SELECT * FROM Searchable WHERE `key` IN (:keys)") + fun getFromKeys(keys: List): List + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertReplaceExisting(toDatabaseEntity: FavoritesItemEntity) diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt index ac30b67e..4708e6a2 100644 --- a/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt +++ b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt @@ -41,6 +41,18 @@ interface FavoritesRepository { fun getHiddenItemKeys(): Flow> fun remove(searchable: Searchable) + /** + * 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: Searchable) + + /** + * Get items with the given keys from the favorites database. + * Items that don't exist in the database will not be returned. + */ + fun getFromKeys(keys: List): List + suspend fun export(toDir: File) suspend fun import(fromDir: File) } @@ -203,6 +215,21 @@ internal class FavoritesRepositoryImpl( } } + override fun save(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + val entity = FavoritesItem( + key = searchable.key, + searchable = searchable, + launchCount = 0, + pinPosition = 0, + hidden = false, + ).toDatabaseEntity() ?: return@withContext + database.searchDao().insertSkipExisting(entity) + } + } + } + private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem { val deserializer: SearchableDeserializer = @@ -216,6 +243,11 @@ internal class FavoritesRepositoryImpl( ) } + override fun getFromKeys(keys: List): List { + val dao = database.searchDao() + return dao.getFromKeys(keys).mapNotNull { fromDatabaseEntity(it).searchable } + } + override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { val dao = database.backupDao() var page = 0 @@ -246,7 +278,8 @@ internal class FavoritesRepositoryImpl( val dao = database.backupDao() dao.wipeFavorites() - val files = fromDir.listFiles { _, name -> name.startsWith("favorites.") } ?: return@withContext + val files = + fromDir.listFiles { _, name -> name.startsWith("favorites.") } ?: return@withContext for (file in files) { val favorites = mutableListOf() diff --git a/permissions/build.gradle.kts b/permissions/build.gradle.kts index 49d45264..f0906394 100644 --- a/permissions/build.gradle.kts +++ b/permissions/build.gradle.kts @@ -43,7 +43,6 @@ dependencies { implementation(project(":ktx")) implementation(project(":base")) - implementation(project(":icons")) implementation(project(":search")) implementation(project(":crashreporter")) } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index c60df06d..bfc70249 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -113,49 +113,55 @@ class SearchVM : ViewModel(), KoinComponent { hideFavorites.postValue(query.isNotEmpty()) searchJob = viewModelScope.launch { isSearching.postValue(true) + val customAttrResults = customAttributesRepository.search(query) + .combine(dataStore.data) { items, settings -> + items.filter { + it is Application + || it is Contact && settings.contactsSearch.enabled + || it is CalendarEvent && settings.calendarSearch.enabled + || it is AppShortcut && settings.appShortcutSearch.enabled + || it is LocalFile && settings.fileSearch.localFiles + || it is GDriveFile && settings.fileSearch.gdrive + || it is OneDriveFile && settings.fileSearch.onedrive + } + } val jobs = mutableListOf>() jobs += async(Dispatchers.Default) { appRepository .search(query) + .withCustomAttributeResults(customAttrResults) .withCustomLabels() .sorted() - .collectLatest { apps -> - hiddenItemKeys.collectLatest { hiddenKeys -> - val results = apps.partition { !hiddenKeys.contains(it.key) } - appResults.postValue(results.first) - hiddenItems.update { - it.copy(apps = results.second) - } + .collectWithHiddenItems(hiddenItemKeys) { results, hidden -> + appResults.postValue(results) + hiddenItems.update { + it.copy(apps = hidden) } } } jobs += async(Dispatchers.Default) { contactRepository .search(query) + .withCustomAttributeResults(customAttrResults) .withCustomLabels() .sorted() - .collectLatest { contacts -> - hiddenItemKeys.collectLatest { hiddenKeys -> - val results = contacts.partition { !hiddenKeys.contains(it.key) } - contactResults.postValue(results.first) - hiddenItems.update { - it.copy(contacts = results.second) - } + .collectWithHiddenItems(hiddenItemKeys) { results, hidden -> + contactResults.postValue(results) + hiddenItems.update { + it.copy(contacts = hidden) } } } jobs += async(Dispatchers.Default) { calendarRepository .search(query) + .withCustomAttributeResults(customAttrResults) .withCustomLabels() .sorted() - .collectLatest { events -> - hiddenItemKeys.collectLatest { hiddenKeys -> - val results = events.partition { !hiddenKeys.contains(it.key) } - calendarResults.postValue(results.first) - hiddenItems.update { - it.copy(calendarEvents = results.second) - } + .collectWithHiddenItems(hiddenItemKeys) { results, hidden -> + calendarResults.postValue(results) + hiddenItems.update { + it.copy(calendarEvents = hidden) } } } @@ -182,15 +188,13 @@ class SearchVM : ViewModel(), KoinComponent { jobs += async(Dispatchers.Default) { fileRepository .search(query) + .withCustomAttributeResults(customAttrResults) .withCustomLabels() .sorted() - .collectLatest { files -> - hiddenItemKeys.collectLatest { hiddenKeys -> - val results = files.partition { !hiddenKeys.contains(it.key) } - fileResults.postValue(results.first) - hiddenItems.update { - it.copy(files = results.second) - } + .collectWithHiddenItems(hiddenItemKeys) { results, hidden -> + fileResults.postValue(results) + hiddenItems.update { + it.copy(files = hidden) } } } @@ -202,11 +206,13 @@ class SearchVM : ViewModel(), KoinComponent { jobs += async(Dispatchers.Default) { appShortcutRepository .search(query) + .withCustomAttributeResults(customAttrResults) .withCustomLabels() .sorted() - .collectLatest { shortcuts -> - hiddenItemKeys.collectLatest { hidden -> - appShortcutResults.postValue(shortcuts.filter { !hidden.contains(it.key) }) + .collectWithHiddenItems(hiddenItemKeys) { results, hidden -> + appShortcutResults.postValue(results) + hiddenItems.update { + it.copy(appShortcuts = hidden) } } } @@ -313,7 +319,27 @@ class SearchVM : ViewModel(), KoinComponent { } } - private fun Flow>.sorted(): Flow> = this.map { it.sorted() } + private inline fun Flow>.withCustomAttributeResults( + customAttributeResults: Flow> + ): Flow> { + return this.combine(customAttributeResults) { items, items2 -> + (items + items2.filterIsInstance()).distinctBy { it.key } + } + } + + private suspend fun Flow>.collectWithHiddenItems( + hiddenItemKeys: Flow>, + action: (items: List, hidden: List) -> Unit + ) { + return collectLatest { items -> + hiddenItemKeys.collectLatest { hiddenKeys -> + val (results, hidden) = items.partition { !hiddenKeys.contains(it.key) } + action(results, hidden) + } + } + } + + private fun Flow>.sorted(): Flow> = this.map { it.sorted() } }