Refactor favorites and searchable database

This commit is contained in:
MM20 2023-04-10 22:44:05 +02:00
parent 582c94b6af
commit ed70097c38
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
49 changed files with 1238 additions and 501 deletions

View File

@ -132,7 +132,7 @@ dependencies {
implementation(project(":core:crashreporter")) implementation(project(":core:crashreporter"))
implementation(project(":data:currencies")) implementation(project(":data:currencies"))
implementation(project(":data:customattrs")) implementation(project(":data:customattrs"))
implementation(project(":data:favorites")) implementation(project(":data:searchable"))
implementation(project(":data:files")) implementation(project(":data:files"))
implementation(project(":libs:g-services")) implementation(project(":libs:g-services"))
implementation(project(":core:i18n")) implementation(project(":core:i18n"))
@ -157,6 +157,7 @@ dependencies {
implementation(project(":data:search-actions")) implementation(project(":data:search-actions"))
implementation(project(":services:global-actions")) implementation(project(":services:global-actions"))
implementation(project(":services:widgets")) implementation(project(":services:widgets"))
implementation(project(":services:favorites"))
// Uncomment this if you want annoying notifications in your debug builds yelling at you how terrible your code is // Uncomment this if you want annoying notifications in your debug builds yelling at you how terrible your code is
//debugImplementation(libs.leakcanary) //debugImplementation(libs.leakcanary)

View File

@ -13,7 +13,7 @@ import de.mm20.launcher2.calculator.calculatorModule
import de.mm20.launcher2.calendar.calendarModule import de.mm20.launcher2.calendar.calendarModule
import de.mm20.launcher2.contacts.contactsModule import de.mm20.launcher2.contacts.contactsModule
import de.mm20.launcher2.data.customattrs.customAttrsModule import de.mm20.launcher2.data.customattrs.customAttrsModule
import de.mm20.launcher2.favorites.favoritesModule import de.mm20.launcher2.searchable.searchableModule
import de.mm20.launcher2.files.filesModule import de.mm20.launcher2.files.filesModule
import de.mm20.launcher2.icons.iconsModule import de.mm20.launcher2.icons.iconsModule
import de.mm20.launcher2.music.musicModule import de.mm20.launcher2.music.musicModule
@ -29,6 +29,7 @@ import de.mm20.launcher2.notifications.notificationsModule
import de.mm20.launcher2.permissions.permissionsModule import de.mm20.launcher2.permissions.permissionsModule
import de.mm20.launcher2.preferences.preferencesModule import de.mm20.launcher2.preferences.preferencesModule
import de.mm20.launcher2.searchactions.searchActionsModule import de.mm20.launcher2.searchactions.searchActionsModule
import de.mm20.launcher2.services.favorites.favoritesModule
import de.mm20.launcher2.services.tags.servicesTagsModule import de.mm20.launcher2.services.tags.servicesTagsModule
import de.mm20.launcher2.services.widgets.widgetsServiceModule import de.mm20.launcher2.services.widgets.widgetsServiceModule
import de.mm20.launcher2.weather.weatherModule import de.mm20.launcher2.weather.weatherModule
@ -67,6 +68,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
customAttrsModule, customAttrsModule,
databaseModule, databaseModule,
favoritesModule, favoritesModule,
searchableModule,
filesModule, filesModule,
globalActionsModule, globalActionsModule,
iconsModule, iconsModule,

View File

@ -1,23 +1,21 @@
package de.mm20.launcher2.activity package de.mm20.launcher2.activity
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.pm.LauncherApps
import android.os.Bundle import android.os.Bundle
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherShortcut import de.mm20.launcher2.services.favorites.FavoritesService
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
class AddItemActivity : Activity() { class AddItemActivity : Activity() {
val favoritesRepository: FavoritesRepository by inject() private val favoritesService: FavoritesService by inject()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val shortcut = AppShortcut.fromPinRequestIntent(this, intent) val shortcut = AppShortcut.fromPinRequestIntent(this, intent)
if (shortcut != null) { if (shortcut != null) {
favoritesRepository.pinItem(shortcut) favoritesService.pinItem(shortcut)
} }
finish() finish()
} }

View File

@ -133,7 +133,7 @@ dependencies {
implementation(project(":data:calculator")) implementation(project(":data:calculator"))
implementation(project(":data:files")) implementation(project(":data:files"))
implementation(project(":data:widgets")) implementation(project(":data:widgets"))
implementation(project(":data:favorites")) implementation(project(":data:searchable"))
implementation(project(":data:wikipedia")) implementation(project(":data:wikipedia"))
implementation(project(":services:badges")) implementation(project(":services:badges"))
implementation(project(":core:crashreporter")) implementation(project(":core:crashreporter"))
@ -151,4 +151,5 @@ dependencies {
implementation(project(":data:search-actions")) implementation(project(":data:search-actions"))
implementation(project(":services:global-actions")) implementation(project(":services:global-actions"))
implementation(project(":services:widgets")) implementation(project(":services:widgets"))
implementation(project(":services:favorites"))
} }

View File

@ -4,10 +4,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.data.customattrs.utils.withCustomLabels import de.mm20.launcher2.data.customattrs.utils.withCustomLabels
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.widgets.CalendarWidget import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.WidgetRepository import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -16,7 +17,7 @@ import org.koin.core.component.inject
abstract class FavoritesVM : ViewModel(), KoinComponent { abstract class FavoritesVM : ViewModel(), KoinComponent {
private val favoritesRepository: FavoritesRepository by inject() private val favoritesService: FavoritesService by inject()
internal val widgetRepository: WidgetRepository by inject() internal val widgetRepository: WidgetRepository by inject()
private val customAttributesRepository: CustomAttributesRepository by inject() private val customAttributesRepository: CustomAttributesRepository by inject()
internal val dataStore: LauncherDataStore by inject() internal val dataStore: LauncherDataStore by inject()
@ -26,7 +27,7 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
val showEditButton = dataStore.data.map { it.favorites.editButton } val showEditButton = dataStore.data.map { it.favorites.editButton }
abstract val tagsExpanded: Flow<Boolean> abstract val tagsExpanded: Flow<Boolean>
val pinnedTags = favoritesRepository.getFavorites( val pinnedTags = favoritesService.getFavorites(
includeTypes = listOf("tag"), includeTypes = listOf("tag"),
manuallySorted = true, manuallySorted = true,
automaticallySorted = true, automaticallySorted = true,
@ -55,7 +56,7 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
val includeFrequentlyUsed = it[2] as Boolean val includeFrequentlyUsed = it[2] as Boolean
val frequentlyUsedRows = it[3] as Int val frequentlyUsedRows = it[3] as Int
val pinned = favoritesRepository.getFavorites( val pinned = favoritesService.getFavorites(
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"), excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
manuallySorted = true, manuallySorted = true,
automaticallySorted = true, automaticallySorted = true,
@ -63,7 +64,7 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
) )
if (includeFrequentlyUsed) { if (includeFrequentlyUsed) {
emitAll(pinned.flatMapLatest { pinned -> emitAll(pinned.flatMapLatest { pinned ->
favoritesRepository.getFavorites( favoritesService.getFavorites(
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"), excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
frequentlyUsed = true, frequentlyUsed = true,
limit = frequentlyUsedRows * columns - pinned.size % columns, limit = frequentlyUsedRows * columns - pinned.size % columns,

View File

@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.launcher
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.graphics.Rect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -11,7 +10,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.globalactions.GlobalActionsService import de.mm20.launcher2.globalactions.GlobalActionsService
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
@ -38,7 +37,7 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
private val globalActionsService: GlobalActionsService by inject() private val globalActionsService: GlobalActionsService by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val favoritesRepository: FavoritesRepository by inject() private val searchableRepository: SearchableRepository by inject()
private var isSystemInDarkMode = MutableStateFlow(false) private var isSystemInDarkMode = MutableStateFlow(false)
@ -137,7 +136,7 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
swipeDownAppKey, swipeDownAppKey,
longPressAppKey, longPressAppKey,
doubleTapAppKey doubleTapAppKey
).let { favoritesRepository.getFromKeys(it) } ).let { searchableRepository.getByKeys(it) }
GestureState( GestureState(
swipeLeftAction = swipeLeftAction, swipeLeftAction = swipeLeftAction,

View File

@ -5,8 +5,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.favorites.SavedSearchableRankInfo
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
@ -24,6 +23,7 @@ import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.search.data.Website import de.mm20.launcher2.search.data.Website
import de.mm20.launcher2.search.data.Wikipedia import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -42,7 +42,8 @@ import org.koin.core.component.inject
class SearchVM : ViewModel(), KoinComponent { class SearchVM : ViewModel(), KoinComponent {
private val favoritesRepository: FavoritesRepository by inject() private val favoritesService: FavoritesService by inject()
private val searchableRepository: SearchableRepository by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
@ -71,8 +72,10 @@ class SearchVM : ViewModel(), KoinComponent {
val favoritesEnabled = dataStore.data.map { it.favorites.enabled } val favoritesEnabled = dataStore.data.map { it.favorites.enabled }
val hideFavorites = mutableStateOf(false) val hideFavorites = mutableStateOf(false)
private val hiddenItemKeys = favoritesRepository private val hiddenItemKeys = searchableRepository
.getHiddenItemKeys() .getKeys(
hidden = true,
)
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
val bestMatch = mutableStateOf<Searchable?>(null) val bestMatch = mutableStateOf<Searchable?>(null)
@ -85,7 +88,7 @@ class SearchVM : ViewModel(), KoinComponent {
val bestMatch = bestMatch.value val bestMatch = bestMatch.value
if (bestMatch is SavableSearchable) { if (bestMatch is SavableSearchable) {
bestMatch.launch(context, null) bestMatch.launch(context, null)
favoritesRepository.incrementLaunchCounter(bestMatch) favoritesService.reportLaunch(bestMatch)
return return
} else if (bestMatch is SearchAction) { } else if (bestMatch is SearchAction) {
bestMatch.start(context) bestMatch.start(context)
@ -144,11 +147,11 @@ class SearchVM : ViewModel(), KoinComponent {
val keys = resultsList.mapNotNull { (it as? SavableSearchable)?.key } val keys = resultsList.mapNotNull { (it as? SavableSearchable)?.key }
when (settings.resultOrdering.ordering) { when (settings.resultOrdering.ordering) {
Ordering.LaunchCount -> favoritesRepository.sortByRelevance( Ordering.LaunchCount -> searchableRepository.sortByRelevance(
keys keys
).first() ).first()
Ordering.Weighted -> favoritesRepository.sortByWeight( Ordering.Weighted -> searchableRepository.sortByWeight(
keys keys
).first() ).first()

View File

@ -120,15 +120,15 @@ class AppItemVM(
} }
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> { fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
return favoritesRepository.isPinned(shortcut) return searchableRepository.isPinned(shortcut)
} }
fun pinShortcut(shortcut: AppShortcut) { fun pinShortcut(shortcut: AppShortcut) {
favoritesRepository.pinItem(shortcut) favoritesService.pinItem(shortcut)
} }
fun unpinShortcut(shortcut: AppShortcut) { fun unpinShortcut(shortcut: AppShortcut) {
favoritesRepository.unpinItem(shortcut) favoritesService.unpinItem(shortcut)
} }
fun launchShortcut(context: Context, shortcut: AppShortcut) { fun launchShortcut(context: Context, shortcut: AppShortcut) {

View File

@ -6,44 +6,42 @@ import androidx.compose.ui.geometry.Rect
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import de.mm20.launcher2.badges.BadgeRepository import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
abstract class SearchableItemVM( abstract class SearchableItemVM(
private val searchable: SavableSearchable private val searchable: SavableSearchable
) : KoinComponent { ) : KoinComponent {
protected val favoritesRepository: FavoritesRepository by inject() protected val favoritesService: FavoritesService by inject()
protected val searchableRepository: SearchableRepository by inject()
protected val badgeRepository: BadgeRepository by inject() protected val badgeRepository: BadgeRepository by inject()
protected val iconRepository: IconRepository by inject() protected val iconRepository: IconRepository by inject()
protected val customAttributesRepository: CustomAttributesRepository by inject() protected val customAttributesRepository: CustomAttributesRepository by inject()
val isPinned = favoritesRepository.isPinned(searchable) val isPinned = searchableRepository.isPinned(searchable)
fun pin() { fun pin() {
favoritesRepository.pinItem(searchable) favoritesService.pinItem(searchable)
} }
fun unpin() { fun unpin() {
favoritesRepository.unpinItem(searchable) favoritesService.unpinItem(searchable)
} }
val isHidden = favoritesRepository.isHidden(searchable) val isHidden = searchableRepository.isHidden(searchable)
fun hide() { fun hide() {
favoritesRepository.hideItem(searchable) searchableRepository.upsert(searchable, hidden = true, pinned = false)
} }
fun unhide() { fun unhide() {
favoritesRepository.unhideItem(searchable) searchableRepository.update(searchable, hidden = false)
} }
val badge = badgeRepository.getBadge(searchable) val badge = badgeRepository.getBadge(searchable)
@ -71,10 +69,10 @@ abstract class SearchableItemVM(
} }
val bundle = options.toBundle() val bundle = options.toBundle()
if (searchable.launch(context, bundle)) { if (searchable.launch(context, bundle)) {
favoritesRepository.incrementLaunchCounter(searchable) favoritesService.reportLaunch(searchable)
return true return true
} else if (searchable is LauncherApp || searchable is AppShortcut) { } else if (searchable is LauncherApp || searchable is AppShortcut) {
favoritesRepository.remove(searchable) searchableRepository.delete(searchable)
} }
return false return false
} }

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import android.util.Log
import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
@ -37,6 +36,6 @@ class ShortcutItemVM(private val shortcut: AppShortcut) : SearchableItemVM(short
fun deleteShortcut() { fun deleteShortcut() {
if (!canDelete) return if (!canDelete) return
if (shortcut is LauncherShortcut) shortcutRepository.removePinnedShortcut(shortcut) if (shortcut is LauncherShortcut) shortcutRepository.removePinnedShortcut(shortcut)
favoritesRepository.unpinItem(shortcut) searchableRepository.delete(shortcut)
} }
} }

View File

@ -13,7 +13,7 @@ import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeRepository import de.mm20.launcher2.badges.BadgeRepository
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.normalize import de.mm20.launcher2.ktx.normalize
@ -24,6 +24,7 @@ import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@ -34,7 +35,7 @@ import org.koin.core.component.inject
class EditFavoritesSheetVM : ViewModel(), KoinComponent { class EditFavoritesSheetVM : ViewModel(), KoinComponent {
private val repository: FavoritesRepository by inject() private val favoritesService: FavoritesService by inject()
private val shortcutRepository: AppShortcutRepository by inject() private val shortcutRepository: AppShortcutRepository by inject()
private val iconRepository: IconRepository by inject() private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject() private val badgeRepository: BadgeRepository by inject()
@ -58,19 +59,19 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
suspend fun reload(showLoadingIndicator: Boolean = true) { suspend fun reload(showLoadingIndicator: Boolean = true) {
loading.value = showLoadingIndicator loading.value = showLoadingIndicator
manuallySorted = mutableListOf() manuallySorted = mutableListOf()
manuallySorted = repository.getFavorites( manuallySorted = favoritesService.getFavorites(
manuallySorted = true, manuallySorted = true,
excludeTypes = listOf("tag"), excludeTypes = listOf("tag"),
).first().toMutableList() ).first().toMutableList()
automaticallySorted = repository.getFavorites( automaticallySorted = favoritesService.getFavorites(
automaticallySorted = true, automaticallySorted = true,
excludeTypes = listOf("tag"), excludeTypes = listOf("tag"),
).first().toMutableList() ).first().toMutableList()
frequentlyUsed = repository.getFavorites( frequentlyUsed = favoritesService.getFavorites(
frequentlyUsed = true, frequentlyUsed = true,
excludeTypes = listOf("tag"), excludeTypes = listOf("tag"),
).first().toMutableList() ).first().toMutableList()
val pinnedTags = repository.getFavorites( val pinnedTags = favoritesService.getFavorites(
includeTypes = listOf("tag"), includeTypes = listOf("tag"),
manuallySorted = true, manuallySorted = true,
automaticallySorted = true, automaticallySorted = true,
@ -169,7 +170,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
} }
private fun save() { private fun save() {
repository.updateFavorites( favoritesService.updateFavorites(
manuallySorted = buildList { manuallySorted = buildList {
pinnedTags.value?.let { addAll(it) } pinnedTags.value?.let { addAll(it) }
addAll(manuallySorted) addAll(manuallySorted)
@ -236,7 +237,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
val item = val item =
gridItems.find { it is FavoritesSheetGridItem.Favorite && it.item.key == key } as FavoritesSheetGridItem.Favorite? gridItems.find { it is FavoritesSheetGridItem.Favorite && it.item.key == key } as FavoritesSheetGridItem.Favorite?
if (item != null) { if (item != null) {
repository.removeFromFavorites(item.item) favoritesService.reset(item.item)
automaticallySorted.removeAll { it.key == item.item.key } automaticallySorted.removeAll { it.key == item.item.key }
|| manuallySorted.removeAll { it.key == item.item.key } || manuallySorted.removeAll { it.key == item.item.key }
|| frequentlyUsed.removeAll { it.key == item.item.key } || frequentlyUsed.removeAll { it.key == item.item.key }
@ -310,9 +311,9 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
|| frequentlyUsed.removeAll { it.key == item.item.key } || frequentlyUsed.removeAll { it.key == item.item.key }
buildItemList() buildItemList()
customAttributesRepository.addTag(item.item, tag) customAttributesRepository.addTag(item.item, tag)
repository.unpinItem(item.item) favoritesService.unpinItem(item.item)
viewModelScope.launch { viewModelScope.launch {
frequentlyUsed = repository.getFavorites( frequentlyUsed = favoritesService.getFavorites(
frequentlyUsed = true, frequentlyUsed = true,
excludeTypes = listOf("tag"), excludeTypes = listOf("tag"),
).first().toMutableList() ).first().toMutableList()

View File

@ -10,12 +10,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.calendar.CalendarRepository import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -31,11 +32,12 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
private val calendarRepository: CalendarRepository by inject() private val calendarRepository: CalendarRepository by inject()
private val favoritesRepository: FavoritesRepository by inject() private val favoritesService: FavoritesService by inject()
private val searchableRepository: SearchableRepository by inject()
val calendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList()) val calendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList())
val pinnedCalendarEvents = val pinnedCalendarEvents =
favoritesRepository.getFavorites( favoritesService.getFavorites(
includeTypes = listOf(CalendarEvent.Domain), includeTypes = listOf(CalendarEvent.Domain),
automaticallySorted = true, automaticallySorted = true,
manuallySorted = true, manuallySorted = true,
@ -158,7 +160,10 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
excludeAllDayEvents = settings.hideAlldayEvents, excludeAllDayEvents = settings.hideAlldayEvents,
excludeCalendars = settings.excludeCalendarsList excludeCalendars = settings.excludeCalendarsList
).collectLatest { events -> ).collectLatest { events ->
favoritesRepository.getHiddenCalendarEventKeys().collectLatest { hidden -> searchableRepository.getKeys(
includeTypes = listOf(CalendarEvent.Domain),
hidden = true,
).collectLatest { hidden ->
upcomingEvents = events.filter { !hidden.contains(it.key) } upcomingEvents = events.filter { !hidden.contains(it.key) }
} }
} }

View File

@ -5,16 +5,16 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.widgets.CalendarWidget import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.WidgetRepository import de.mm20.launcher2.widgets.WidgetRepository
@ -26,7 +26,7 @@ import org.koin.core.component.inject
class FavoritesPartProvider : PartProvider, KoinComponent { class FavoritesPartProvider : PartProvider, KoinComponent {
private val favoritesRepository: FavoritesRepository by inject() private val favoritesService: FavoritesService by inject()
private val widgetRepository: WidgetRepository by inject() private val widgetRepository: WidgetRepository by inject()
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
@ -47,7 +47,7 @@ class FavoritesPartProvider : PartProvider, KoinComponent {
) )
val favorites by remember(columns, excludeCalendar, layout) { val favorites by remember(columns, excludeCalendar, layout) {
favoritesRepository.getFavorites( favoritesService.getFavorites(
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"), excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
manuallySorted = true, manuallySorted = true,
automaticallySorted = true, automaticallySorted = true,

View File

@ -1,19 +1,17 @@
package de.mm20.launcher2.ui.settings.debug package de.mm20.launcher2.ui.settings.debug
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
class DebugSettingsScreenVM: ViewModel(), KoinComponent { class DebugSettingsScreenVM: ViewModel(), KoinComponent {
private val favoritesRepository: FavoritesRepository by inject() private val searchableRepository: SearchableRepository by inject()
private val customAttributesRepository: CustomAttributesRepository by inject() private val customAttributesRepository: CustomAttributesRepository by inject()
suspend fun cleanUpDatabase(): Int { suspend fun cleanUpDatabase(): Int {
var removed = favoritesRepository.cleanupDatabase() var removed = searchableRepository.cleanupDatabase()
removed += customAttributesRepository.cleanupDatabase() removed += customAttributesRepository.cleanupDatabase()
return removed return removed
} }

View File

@ -4,7 +4,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
@ -14,10 +14,8 @@ import de.mm20.launcher2.preferences.Settings.GestureSettings.GestureAction
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -26,7 +24,7 @@ import org.koin.core.component.inject
class GestureSettingsScreenVM : ViewModel(), KoinComponent { class GestureSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val favoritesRepository: FavoritesRepository by inject() private val searchableRepository: SearchableRepository by inject()
private val iconRepository: IconRepository by inject() private val iconRepository: IconRepository by inject()
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility).asLiveData() val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility).asLiveData()
@ -86,13 +84,13 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val swipeLeftApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeLeftApp } val swipeLeftApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeLeftApp }
.map { .map {
if (it.isEmpty()) null else favoritesRepository.getFromKeys(listOf(it)).firstOrNull() if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
} }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setSwipeLeftApp(searchable: SavableSearchable?) { fun setSwipeLeftApp(searchable: SavableSearchable?) {
viewModelScope.launch { viewModelScope.launch {
searchable?.let { favoritesRepository.save(it) } searchable?.let { searchableRepository.insert(it) }
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder()
.setGestures(it.gestures.toBuilder() .setGestures(it.gestures.toBuilder()
@ -106,13 +104,13 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val swipeRightApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeRightApp } val swipeRightApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeRightApp }
.map { .map {
if (it.isEmpty()) null else favoritesRepository.getFromKeys(listOf(it)).firstOrNull() if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
} }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setSwipeRightApp(searchable: SavableSearchable?) { fun setSwipeRightApp(searchable: SavableSearchable?) {
viewModelScope.launch { viewModelScope.launch {
searchable?.let { favoritesRepository.save(it) } searchable?.let { searchableRepository.insert(it) }
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder()
.setGestures(it.gestures.toBuilder() .setGestures(it.gestures.toBuilder()
@ -126,13 +124,13 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val swipeDownApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeDownApp } val swipeDownApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeDownApp }
.map { .map {
if (it.isEmpty()) null else favoritesRepository.getFromKeys(listOf(it)).firstOrNull() if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
} }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setSwipeDownApp(searchable: SavableSearchable?) { fun setSwipeDownApp(searchable: SavableSearchable?) {
viewModelScope.launch { viewModelScope.launch {
searchable?.let { favoritesRepository.save(it) } searchable?.let { searchableRepository.insert(it) }
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder()
.setGestures(it.gestures.toBuilder() .setGestures(it.gestures.toBuilder()
@ -146,13 +144,13 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val longPressApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.longPressApp } val longPressApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.longPressApp }
.map { .map {
if (it.isEmpty()) null else favoritesRepository.getFromKeys(listOf(it)).firstOrNull() if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
} }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setLongPressApp(searchable: SavableSearchable?) { fun setLongPressApp(searchable: SavableSearchable?) {
viewModelScope.launch { viewModelScope.launch {
searchable?.let { favoritesRepository.save(it) } searchable?.let { searchableRepository.insert(it) }
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder()
.setGestures(it.gestures.toBuilder() .setGestures(it.gestures.toBuilder()
@ -166,13 +164,13 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val doubleTapApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.doubleTapApp } val doubleTapApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.doubleTapApp }
.map { .map {
if (it.isEmpty()) null else favoritesRepository.getFromKeys(listOf(it)).firstOrNull() if (it.isEmpty()) null else searchableRepository.getByKeys(listOf(it)).firstOrNull()
} }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(stopTimeoutMillis = 10000), null)
fun setDoubleTapApp(searchable: SavableSearchable?) { fun setDoubleTapApp(searchable: SavableSearchable?) {
viewModelScope.launch { viewModelScope.launch {
searchable?.let { favoritesRepository.save(it) } searchable?.let { searchableRepository.insert(it) }
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder()
.setGestures(it.gestures.toBuilder() .setGestures(it.gestures.toBuilder()

View File

@ -10,7 +10,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData import androidx.lifecycle.liveData
import de.mm20.launcher2.applications.AppRepository import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
@ -26,26 +26,26 @@ import org.koin.core.component.inject
class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent { class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
private val appRepository: AppRepository by inject() private val appRepository: AppRepository by inject()
private val favoritesRepository: FavoritesRepository by inject() private val searchableRepository: SearchableRepository by inject()
private val iconRepository: IconRepository by inject() private val iconRepository: IconRepository by inject()
val allApps = appRepository.getAllInstalledApps().map { val allApps = appRepository.getAllInstalledApps().map {
withContext(Dispatchers.Default) { it.sorted() } withContext(Dispatchers.Default) { it.sorted() }
}.asLiveData() }.asLiveData()
val hiddenItems: LiveData<List<SavableSearchable>> = liveData { val hiddenItems: LiveData<List<SavableSearchable>> = liveData {
val hidden = favoritesRepository.getHiddenItems().first().filter { it !is LauncherApp }.sorted() val hidden = searchableRepository.get(hidden = true).first().filter { it !is LauncherApp }.sorted()
emit(hidden) emit(hidden)
} }
fun isHidden(searchable: SavableSearchable): Flow<Boolean> { fun isHidden(searchable: SavableSearchable): Flow<Boolean> {
return favoritesRepository.isHidden(searchable) return searchableRepository.isHidden(searchable)
} }
fun setHidden(searchable: SavableSearchable, hidden: Boolean) { fun setHidden(searchable: SavableSearchable, hidden: Boolean) {
if(hidden) { if(hidden) {
favoritesRepository.hideItem(searchable) searchableRepository.upsert(searchable, hidden = true, pinned = false)
} else { } else {
favoritesRepository.unhideItem(searchable) searchableRepository.update(searchable, hidden = false)
} }
} }

View File

@ -0,0 +1,496 @@
{
"formatVersion": 1,
"database": {
"version": 24,
"identityHash": "fd75b8c610f9b92eeb42a3ba1474914d",
"entities": [
{
"tableName": "forecasts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))",
"fields": [
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "temperature",
"columnName": "temperature",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "minTemp",
"columnName": "minTemp",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "maxTemp",
"columnName": "maxTemp",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "pressure",
"columnName": "pressure",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "humidity",
"columnName": "humidity",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "condition",
"columnName": "condition",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "clouds",
"columnName": "clouds",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "windSpeed",
"columnName": "windSpeed",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "windDirection",
"columnName": "windDirection",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "precipitation",
"columnName": "rain",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "snow",
"columnName": "snow",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "night",
"columnName": "night",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "location",
"columnName": "location",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "provider",
"columnName": "provider",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "providerUrl",
"columnName": "providerUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "precipProbability",
"columnName": "rainProbability",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "snowProbability",
"columnName": "snowProbability",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updateTime",
"columnName": "updateTime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"timestamp"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Searchable",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL DEFAULT 0, `pinPosition` INTEGER NOT NULL DEFAULT 0, `hidden` INTEGER NOT NULL DEFAULT 0, `weight` REAL NOT NULL DEFAULT 0.0, PRIMARY KEY(`key`))",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serializedSearchable",
"columnName": "searchable",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "launchCount",
"columnName": "launchCount",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "pinPosition",
"columnName": "pinPosition",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "hidden",
"columnName": "hidden",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "weight",
"columnName": "weight",
"affinity": "REAL",
"notNull": true,
"defaultValue": "0.0"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"key"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Currency",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))",
"fields": [
{
"fieldPath": "symbol",
"columnName": "symbol",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "lastUpdate",
"columnName": "lastUpdate",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"symbol"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Icons",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `packageName` TEXT, `activityName` TEXT, `drawable` TEXT, `extras` TEXT, `iconPack` TEXT NOT NULL, `name` TEXT, `themed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
"fields": [
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "activityName",
"columnName": "activityName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "drawable",
"columnName": "drawable",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "extras",
"columnName": "extras",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "iconPack",
"columnName": "iconPack",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "themed",
"columnName": "themed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "IconPack",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, `themed` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
"fields": [
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scale",
"columnName": "scale",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "themed",
"columnName": "themed",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Widget",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `config` TEXT, `position` INTEGER NOT NULL, `id` BLOB NOT NULL, `parentId` BLOB, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "config",
"columnName": "config",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "parentId",
"columnName": "parentId",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "CustomAttributes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `value` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SearchAction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `type` TEXT NOT NULL, `data` TEXT, `label` TEXT, `icon` INTEGER, `color` INTEGER, `customIcon` TEXT, `options` TEXT, PRIMARY KEY(`position`))",
"fields": [
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "customIcon",
"columnName": "customIcon",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "options",
"columnName": "options",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"position"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fd75b8c610f9b92eeb42a3ba1474914d')"
]
}
}

View File

@ -22,6 +22,7 @@ import de.mm20.launcher2.database.migrations.Migration_19_20
import de.mm20.launcher2.database.migrations.Migration_20_21 import de.mm20.launcher2.database.migrations.Migration_20_21
import de.mm20.launcher2.database.migrations.Migration_21_22 import de.mm20.launcher2.database.migrations.Migration_21_22
import de.mm20.launcher2.database.migrations.Migration_22_23 import de.mm20.launcher2.database.migrations.Migration_22_23
import de.mm20.launcher2.database.migrations.Migration_23_24
import de.mm20.launcher2.database.migrations.Migration_6_7 import de.mm20.launcher2.database.migrations.Migration_6_7
import de.mm20.launcher2.database.migrations.Migration_7_8 import de.mm20.launcher2.database.migrations.Migration_7_8
import de.mm20.launcher2.database.migrations.Migration_8_9 import de.mm20.launcher2.database.migrations.Migration_8_9
@ -39,14 +40,15 @@ import java.util.UUID
WidgetEntity::class, WidgetEntity::class,
CustomAttributeEntity::class, CustomAttributeEntity::class,
SearchActionEntity::class SearchActionEntity::class
], version = 23, exportSchema = true ], version = 24, exportSchema = true
) )
@TypeConverters(ComponentNameConverter::class, StringListConverter::class) @TypeConverters(ComponentNameConverter::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun weatherDao(): WeatherDao abstract fun weatherDao(): WeatherDao
abstract fun searchDao(): SearchDao
abstract fun iconDao(): IconDao abstract fun iconDao(): IconDao
abstract fun searchableDao(): SearchableDao
abstract fun widgetDao(): WidgetDao abstract fun widgetDao(): WidgetDao
abstract fun currencyDao(): CurrencyDao abstract fun currencyDao(): CurrencyDao
abstract fun backupDao(): BackupRestoreDao abstract fun backupDao(): BackupRestoreDao
@ -114,6 +116,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration_20_21(), Migration_20_21(),
Migration_21_22(), Migration_21_22(),
Migration_22_23(), Migration_22_23(),
Migration_23_24(),
).build() ).build()
if (_instance == null) _instance = instance if (_instance == null) _instance = instance
return instance return instance

View File

@ -1,164 +0,0 @@
package de.mm20.launcher2.database
import androidx.room.*
import de.mm20.launcher2.database.entities.SavedSearchableEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface SearchDao {
@Insert
fun insertAll(items: List<SavedSearchableEntity>)
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertAllSkipExisting(items: List<SavedSearchableEntity>)
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertSkipExisting(items: SavedSearchableEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAllReplaceExisting(items: List<SavedSearchableEntity>)
@Query(
"SELECT * FROM Searchable " +
"WHERE ((:manuallySorted AND pinned > 1) OR " +
"(:automaticallySorted AND pinned = 1) OR" +
"(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" +
") AND hidden = 0 ORDER BY pinned DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getFavorites(
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
limit: Int,
): Flow<List<SavedSearchableEntity>>
@Query(
"SELECT * FROM Searchable " +
"WHERE `type` IN (:includeTypes) AND (" +
"(:manuallySorted AND pinned > 1) OR " +
"(:automaticallySorted AND pinned = 1) OR" +
"(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" +
") AND hidden = 0 ORDER BY pinned DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getFavoritesWithTypes(
includeTypes: List<String>,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
limit: Int,
): Flow<List<SavedSearchableEntity>>
@Query(
"SELECT * FROM Searchable " +
"WHERE `type` NOT IN (:excludeTypes) AND (" +
"(:manuallySorted AND pinned > 1) OR " +
"(:automaticallySorted AND pinned = 1) OR" +
"(:frequentlyUsed AND pinned = 0 AND launchCount > 0)" +
") AND hidden = 0 ORDER BY pinned DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getFavoritesWithoutTypes(
excludeTypes: List<String>,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
limit: Int,
): Flow<List<SavedSearchableEntity>>
@Query("SELECT `key` FROM Searchable WHERE hidden = 1 AND type = 'calendar'")
fun getHiddenCalendarEventKeys(): Flow<List<String>>
@Query("DELETE FROM Searchable WHERE `key` IN (:keys)")
fun deleteAll(keys: List<String>)
@Query("UPDATE Searchable SET pinned = 1, hidden = 0 WHERE `key` = :key")
fun pinExistingItem(key: String)
@Transaction
fun pinToFavorites(item: SavedSearchableEntity) {
pinExistingItem(item.key)
insertSkipExisting(item)
}
@Query("UPDATE Searchable SET pinned = 0 WHERE `key` = :key")
fun unpinFavorite(key: String)
@Query("DELETE FROM Searchable WHERE `key` = :key")
suspend fun deleteByKey(key: String)
@Query("UPDATE Searchable SET pinned = 0 WHERE `key` = :key")
fun unpinApp(key: String)
@Query("SELECT pinned FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinned ORDER BY pinned DESC LIMIT 1")
fun isPinned(key: String): Flow<Boolean>
@Query("UPDATE Searchable SET hidden = 1, pinned = 0 WHERE `key` = :key")
fun hideExistingItem(key: String)
@Transaction
fun hideItem(item: SavedSearchableEntity) {
hideExistingItem(item.key)
insertSkipExisting(item)
}
@Query("UPDATE Searchable SET hidden = 0 WHERE `key` = :key")
fun unhideItem(key: String)
@Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1")
fun isHidden(key: String): Flow<Boolean>
@Query("SELECT `key` FROM SEARCHABLE WHERE hidden = 1")
fun getHiddenItemKeys(): Flow<List<String>>
@Query("SELECT * FROM SEARCHABLE WHERE hidden = 1")
fun getHiddenItems(): Flow<List<SavedSearchableEntity>>
@Query("UPDATE Searchable SET launchCount = launchCount + 1 WHERE `key` = :key")
fun incrementExistingLaunchCount(key: String)
@Transaction
fun incrementLaunchCount(item: SavedSearchableEntity, alpha: Double) {
incrementExistingLaunchCount(item.key)
increaseWeightWhere(item.key, alpha)
reduceWeightExcept(item.key, alpha)
insertSkipExisting(item)
}
@Query("SELECT * FROM Searchable WHERE `key` = :key")
fun getFavorite(key: String): SavedSearchableEntity?
@Query("SELECT * FROM Searchable WHERE `key` IN (:keys)")
suspend fun getFromKeys(keys: List<String>): List<SavedSearchableEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertReplaceExisting(toDatabaseEntity: SavedSearchableEntity)
@Transaction
fun saveFavorites(favorites: List<SavedSearchableEntity>) {
deleteAllFavorites()
insertAll(favorites)
}
@Query("DELETE FROM Searchable WHERE hidden = 0")
fun deleteAllFavorites()
@Query("UPDATE Searchable SET `pinned` = 0")
fun unpinAll()
@Query("UPDATE Searchable SET `pinned` = 0, `launchCount` = 0 WHERE `key` = :key")
suspend fun resetPinStatusAndLaunchCounter(key: String)
@Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) AND launchCount > 0 ORDER BY launchCount DESC, pinned DESC")
fun sortByRelevance(keys: List<String>): Flow<List<String>>
@Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) ORDER BY `weight` DESC, pinned DESC")
fun sortByWeight(keys: List<String>): Flow<List<String>>
@Query("UPDATE Searchable SET `weight` = `weight` * (1.0 - :alpha) WHERE `key` != :key")
fun reduceWeightExcept(key: String, alpha: Double)
@Query("UPDATE Searchable SET `weight` = `weight` + :alpha * (1.0 - `weight`) WHERE `key` == :key")
fun increaseWeightWhere(key: String, alpha: Double)
}

View File

@ -0,0 +1,178 @@
package de.mm20.launcher2.database
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import androidx.room.Upsert
import de.mm20.launcher2.database.entities.SavedSearchableEntity
import de.mm20.launcher2.database.entities.SavedSearchableUpdatePinEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface SearchableDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(searchable: SavedSearchableEntity)
@Upsert(entity = SavedSearchableEntity::class)
suspend fun upsert(searchable: SavedSearchableEntity)
@Upsert(entity = SavedSearchableEntity::class)
suspend fun upsert(searchable: List<SavedSearchableUpdatePinEntity>)
@Update(entity = SavedSearchableEntity::class)
suspend fun update(searchable: SavedSearchableUpdatePinEntity)
@Query(
"SELECT * FROM Searchable " +
"WHERE (" +
"(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" +
") AND hidden = :hidden ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun get(
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
hidden: Boolean = false,
limit: Int,
): Flow<List<SavedSearchableEntity>>
@Query(
"SELECT * FROM Searchable " +
"WHERE (`type` IN (:includeTypes)) AND " +
"(" +
"(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" +
") AND hidden = :hidden ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getIncludeTypes(
includeTypes: List<String>?,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
hidden: Boolean = false,
limit: Int,
): Flow<List<SavedSearchableEntity>>
@Query(
"SELECT * FROM Searchable " +
"WHERE (`type` NOT IN (:excludeTypes)) AND " +
"(" +
"(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" +
") AND hidden = :hidden ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getExcludeTypes(
excludeTypes: List<String>?,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
hidden: Boolean = false,
limit: Int,
): Flow<List<SavedSearchableEntity>>
@Query(
"SELECT `key` FROM Searchable " +
"WHERE (" +
"(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" +
") AND hidden = :hidden ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getKeys(
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
hidden: Boolean = false,
limit: Int,
): Flow<List<String>>
@Query(
"SELECT `key` FROM Searchable " +
"WHERE (`type` IN (:includeTypes)) AND " +
"(" +
"(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" +
") AND hidden = :hidden ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getKeysIncludeTypes(
includeTypes: List<String>?,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
hidden: Boolean = false,
limit: Int,
): Flow<List<String>>
@Query(
"SELECT `key` FROM Searchable " +
"WHERE (`type` NOT IN (:excludeTypes)) AND " +
"(" +
"(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" +
") AND hidden = :hidden ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
)
fun getKeysExcludeTypes(
excludeTypes: List<String>?,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
hidden: Boolean = false,
limit: Int,
): Flow<List<String>>
@Query("SELECT * FROM Searchable WHERE `key` IN (:keys)")
suspend fun getByKeys(keys: List<String>): List<SavedSearchableEntity>
@Query("SELECT * FROM Searchable WHERE `key` = :key")
fun getByKey(key: String): Flow<SavedSearchableEntity?>
@Transaction
suspend fun touch(item: SavedSearchableEntity, alpha: Double) {
incrementLaunchCount(item.key)
increaseWeightWhere(item.key, alpha)
reduceWeightExcept(item.key, alpha)
insert(item)
}
@Query("UPDATE Searchable SET launchCount = launchCount + 1 WHERE `key` = :key")
fun incrementLaunchCount(key: String)
@Query("UPDATE Searchable SET `weight` = `weight` * (1.0 - :alpha) WHERE `key` != :key")
fun reduceWeightExcept(key: String, alpha: Double)
@Query("UPDATE Searchable SET `weight` = `weight` + :alpha * (1.0 - `weight`) WHERE `key` == :key")
fun increaseWeightWhere(key: String, alpha: Double)
@Query("DELETE FROM Searchable WHERE `key` = :key")
suspend fun delete(key: String)
@Query("UPDATE Searchable SET `pinPosition` = 0")
suspend fun unpinAll()
@Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) AND launchCount > 0 ORDER BY launchCount DESC, pinPosition DESC")
fun sortByRelevance(keys: List<String>): Flow<List<String>>
@Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) ORDER BY `weight` DESC, pinPosition DESC")
fun sortByWeight(keys: List<String>): Flow<List<String>>
@Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1")
fun isHidden(key: String): Flow<Boolean>
@Query("SELECT pinPosition FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinPosition ORDER BY pinPosition DESC LIMIT 1")
fun isPinned(key: String): Flow<Boolean>
}

View File

@ -9,8 +9,15 @@ data class SavedSearchableEntity(
@PrimaryKey val key: String, @PrimaryKey val key: String,
val type: String, val type: String,
@ColumnInfo(name = "searchable") val serializedSearchable: String, @ColumnInfo(name = "searchable") val serializedSearchable: String,
var launchCount: Int, @ColumnInfo(defaultValue = "0") val launchCount: Int,
@ColumnInfo(name = "pinned") var pinPosition: Int, @ColumnInfo(defaultValue = "0") val pinPosition: Int,
var hidden: Boolean, @ColumnInfo(defaultValue = "0") val hidden: Boolean,
@ColumnInfo(defaultValue = "0.0") var weight: Double @ColumnInfo(defaultValue = "0.0") val weight: Double
) )
data class SavedSearchableUpdatePinEntity(
val key: String,
val type: String,
@ColumnInfo(name = "searchable") val serializedSearchable: String,
val pinPosition: Int? = null,
)

View File

@ -0,0 +1,31 @@
package de.mm20.launcher2.database.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration_23_24 : Migration(23, 24) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Searchable RENAME TO Searchable_old")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Searchable` (
`key` TEXT NOT NULL,
`type` TEXT NOT NULL,
`searchable` TEXT NOT NULL,
`launchCount` INTEGER NOT NULL DEFAULT 0,
`pinPosition` INTEGER NOT NULL DEFAULT 0,
`hidden` INTEGER NOT NULL DEFAULT 0,
`weight` DOUBLE NOT NULL DEFAULT 0.0,
PRIMARY KEY(`key`)
)
"""
)
database.execSQL(
"""
INSERT INTO `Searchable` (`key`, `type`, `searchable`, `launchCount`, `pinPosition`, `hidden`, `weight`)
SELECT `key`, `type`, `searchable`, `launchCount`, `pinned`, `hidden`, `weight` FROM `Searchable_old`
"""
)
database.execSQL("DROP TABLE Searchable_old")
}
}

View File

@ -44,5 +44,5 @@ dependencies {
implementation(project(":core:base")) implementation(project(":core:base"))
implementation(project(":core:ktx")) implementation(project(":core:ktx"))
implementation(project(":core:crashreporter")) implementation(project(":core:crashreporter"))
implementation(project(":data:favorites")) implementation(project(":data:searchable"))
} }

View File

@ -3,7 +3,7 @@ package de.mm20.launcher2.data.customattrs
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.CustomAttributeEntity 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.ktx.jsonObjectOf
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -46,7 +46,7 @@ interface CustomAttributesRepository {
internal class CustomAttributesRepositoryImpl( internal class CustomAttributesRepositoryImpl(
private val appDatabase: AppDatabase, private val appDatabase: AppDatabase,
private val favoritesRepository: FavoritesRepository private val searchableRepository: SearchableRepository
) : CustomAttributesRepository { ) : CustomAttributesRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default) private val scope = CoroutineScope(Job() + Dispatchers.Default)
@ -79,7 +79,7 @@ internal class CustomAttributesRepositoryImpl(
override fun setCustomLabel(searchable: SavableSearchable, label: String) { override fun setCustomLabel(searchable: SavableSearchable, label: String) {
val dao = appDatabase.customAttrsDao() val dao = appDatabase.customAttrsDao()
scope.launch { scope.launch {
favoritesRepository.save(searchable) searchableRepository.insert(searchable)
appDatabase.runInTransaction { appDatabase.runInTransaction {
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value) dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
dao.setCustomAttribute( dao.setCustomAttribute(
@ -102,7 +102,7 @@ internal class CustomAttributesRepositoryImpl(
override fun setTags(searchable: SavableSearchable, tags: List<String>) { override fun setTags(searchable: SavableSearchable, tags: List<String>) {
val dao = appDatabase.customAttrsDao() val dao = appDatabase.customAttrsDao()
scope.launch { scope.launch {
favoritesRepository.save(searchable) searchableRepository.insert(searchable)
dao.setTags(searchable.key, tags.map { dao.setTags(searchable.key, tags.map {
CustomTag(it).toDatabaseEntity(searchable.key) CustomTag(it).toDatabaseEntity(searchable.key)
}) })
@ -128,7 +128,7 @@ internal class CustomAttributesRepositoryImpl(
override fun getItemsForTag(tag: String): Flow<List<SavableSearchable>> { override fun getItemsForTag(tag: String): Flow<List<SavableSearchable>> {
val dao = appDatabase.customAttrsDao() val dao = appDatabase.customAttrsDao()
return dao.getItemsWithTag(tag).map { return dao.getItemsWithTag(tag).map {
favoritesRepository.getFromKeys(it) searchableRepository.getByKeys(it)
} }
} }
@ -137,7 +137,7 @@ internal class CustomAttributesRepositoryImpl(
return scope.launch { return scope.launch {
dao.setItemsWithTag(tag, items.map { it.key }) dao.setItemsWithTag(tag, items.map { it.key })
for (item in items) { for (item in items) {
favoritesRepository.save(item) searchableRepository.insert(item)
} }
} }
} }
@ -172,7 +172,7 @@ internal class CustomAttributesRepositoryImpl(
} }
val dao = appDatabase.customAttrsDao() val dao = appDatabase.customAttrsDao()
return dao.search("%$query%").map { return dao.search("%$query%").map {
favoritesRepository.getFromKeys(it).toImmutableList() searchableRepository.getByKeys(it).toImmutableList()
} }
} }

View File

@ -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()) }
}

View File

@ -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()) }
}

View File

@ -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.database.entities.SavedSearchableEntity
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.favorites package de.mm20.launcher2.searchable
data class SavedSearchableRankInfo( data class SavedSearchableRankInfo(
val key: String, val key: String,

View File

@ -1,58 +1,87 @@
package de.mm20.launcher2.favorites package de.mm20.launcher2.searchable
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.room.withTransaction
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.SavedSearchableEntity import de.mm20.launcher2.database.entities.SavedSearchableEntity
import de.mm20.launcher2.database.entities.SavedSearchableUpdatePinEntity
import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* 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.JSONArray
import org.json.JSONException import org.json.JSONException
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import java.io.File 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 * Touch a searchable to update its weight and launch counter
* @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. fun touch(
* @param manuallySorted Include items that have been sorted manually searchable: SavableSearchable,
* @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 get(
*/
fun getFavorites(
includeTypes: List<String>? = null, includeTypes: List<String>? = null,
excludeTypes: List<String>? = null, excludeTypes: List<String>? = null,
manuallySorted: Boolean = false, manuallySorted: Boolean = false,
automaticallySorted: Boolean = false, automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false, frequentlyUsed: Boolean = false,
limit: Int = 100 hidden: Boolean = false,
limit: Int = 100,
): Flow<List<SavableSearchable>> ): 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 isPinned(searchable: SavableSearchable): Flow<Boolean>
fun pinItem(searchable: SavableSearchable)
fun unpinItem(searchable: SavableSearchable)
fun isHidden(searchable: SavableSearchable): Flow<Boolean> fun isHidden(searchable: SavableSearchable): Flow<Boolean>
fun hideItem(searchable: SavableSearchable)
fun unhideItem(searchable: SavableSearchable)
fun incrementLaunchCounter(searchable: SavableSearchable)
fun updateFavorites( fun updateFavorites(
manuallySorted: List<SavableSearchable>, manuallySorted: List<SavableSearchable>,
automaticallySorted: List<SavableSearchable>, automaticallySorted: List<SavableSearchable>,
) )
fun getHiddenItems(): Flow<List<SavableSearchable>>
fun getHiddenItemKeys(): Flow<List<String>>
/** /**
* Returns the given keys sorted by relevance. * Returns the given keys sorted by relevance.
* The first item in the list is the most relevant. * The first item in the list is the most relevant.
@ -65,24 +94,13 @@ interface FavoritesRepository {
/** /**
* Remove this item from the Searchable database * Remove this item from the Searchable database
*/ */
fun remove(searchable: SavableSearchable) fun delete(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)
/** /**
* Get items with the given keys from the favorites database. * Get items with the given keys from the favorites database.
* Items that don't exist in the database will not be returned. * 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 export(toDir: File)
suspend fun import(fromDir: File) suspend fun import(fromDir: File)
@ -95,177 +113,193 @@ interface FavoritesRepository {
suspend fun cleanupDatabase(): Int suspend fun cleanupDatabase(): Int
} }
internal class FavoritesRepositoryImpl( internal class SearchableRepositoryImpl(
private val context: Context, private val context: Context,
private val database: AppDatabase, private val database: AppDatabase,
private val dataStore: LauncherDataStore private val dataStore: LauncherDataStore
) : FavoritesRepository, KoinComponent { ) : SearchableRepository, KoinComponent {
private val scope = CoroutineScope(Job() + Dispatchers.Default) 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>?, includeTypes: List<String>?,
excludeTypes: List<String>?, excludeTypes: List<String>?,
manuallySorted: Boolean, manuallySorted: Boolean,
automaticallySorted: Boolean, automaticallySorted: Boolean,
frequentlyUsed: Boolean, frequentlyUsed: Boolean,
hidden: Boolean,
limit: Int limit: Int
): Flow<List<SavableSearchable>> { ): Flow<List<SavableSearchable>> {
val dao = database.searchDao() val dao = database.searchableDao()
val entities = when { val entities = when {
includeTypes == null && excludeTypes == null -> dao.getFavorites( includeTypes == null && excludeTypes == null -> dao.get(
manuallySorted = manuallySorted, manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted, automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed, frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit limit = limit
) )
includeTypes != null && excludeTypes == null -> { includeTypes == null -> dao.getExcludeTypes(
dao.getFavoritesWithTypes( excludeTypes = excludeTypes,
includeTypes = includeTypes, manuallySorted = manuallySorted,
manuallySorted = manuallySorted, automaticallySorted = automaticallySorted,
automaticallySorted = automaticallySorted, frequentlyUsed = frequentlyUsed,
frequentlyUsed = frequentlyUsed, hidden = hidden,
limit = limit limit = limit
) )
}
excludeTypes != null && includeTypes == null -> { excludeTypes == null -> dao.getIncludeTypes(
dao.getFavoritesWithoutTypes( includeTypes = includeTypes,
excludeTypes = excludeTypes, manuallySorted = manuallySorted,
manuallySorted = manuallySorted, automaticallySorted = automaticallySorted,
automaticallySorted = automaticallySorted, frequentlyUsed = frequentlyUsed,
frequentlyUsed = frequentlyUsed, hidden = hidden,
limit = limit 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 { return entities.map {
it.mapNotNull { fromDatabaseEntity(it).searchable } it.mapNotNull { fromDatabaseEntity(it).searchable }
} }
} }
override fun getHiddenCalendarEventKeys(): Flow<List<String>> { override fun getKeys(
return database.searchDao().getHiddenCalendarEventKeys() 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> { override fun isPinned(searchable: SavableSearchable): Flow<Boolean> {
return database.searchDao().isPinned(searchable.key) return database.searchableDao().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)
}
}
} }
override fun isHidden(searchable: SavableSearchable): Flow<Boolean> { 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 { scope.launch {
withContext(Dispatchers.IO) { database.searchableDao().delete(searchable.key)
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)
}
} }
} }
@ -273,51 +307,42 @@ internal class FavoritesRepositoryImpl(
manuallySorted: List<SavableSearchable>, manuallySorted: List<SavableSearchable>,
automaticallySorted: List<SavableSearchable> automaticallySorted: List<SavableSearchable>
) { ) {
val dao = database.searchDao() val dao = database.searchableDao()
scope.launch { scope.launch {
withContext(Dispatchers.IO) { database.withTransaction {
val keys = manuallySorted.map { it.key } + automaticallySorted.map { it.key } dao.unpinAll()
val entities = dao.getFromKeys(keys) dao.upsert(
val updatedManuallySorted = manuallySorted.mapIndexedNotNull { index, searchable -> manuallySorted.mapIndexedNotNull { index, savableSearchable ->
val entity = entities.find { searchable.key == it.key } ?: SavedSearchable( SavedSearchableUpdatePinEntity(
key = searchable.key, key = savableSearchable.key,
searchable = searchable, type = savableSearchable.domain,
launchCount = 0, pinPosition = manuallySorted.size - index + 1,
pinPosition = 0, serializedSearchable = savableSearchable.serialize()
hidden = false, ?: return@mapIndexedNotNull null,
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.runInTransaction { )
dao.unpinAll() dao.upsert(
dao.insertAllReplaceExisting(updatedManuallySorted) automaticallySorted.mapNotNull { savableSearchable ->
dao.insertAllReplaceExisting(updatedAutomaticallySorted) SavedSearchableUpdatePinEntity(
} key = savableSearchable.key,
type = savableSearchable.domain,
pinPosition = 1,
serializedSearchable = savableSearchable.serialize()
?: return@mapNotNull null,
)
}
)
} }
} }
} }
override fun sortByRelevance(keys: List<String>): Flow<List<String>> { 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>> { 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 { private fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable {
@ -337,13 +362,13 @@ internal class FavoritesRepositoryImpl(
private fun removeInvalidItem(key: String) { private fun removeInvalidItem(key: String) {
scope.launch { scope.launch {
database.searchDao().deleteByKey(key) database.searchableDao().delete(key)
} }
} }
override suspend fun getFromKeys(keys: List<String>): List<SavableSearchable> { override suspend fun getByKeys(keys: List<String>): List<SavableSearchable> {
val dao = database.searchDao() val dao = database.searchableDao()
return dao.getFromKeys(keys) return dao.getByKeys(keys)
.mapNotNull { fromDatabaseEntity(it).searchable } .mapNotNull { fromDatabaseEntity(it).searchable }
} }

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.favorites package de.mm20.launcher2.searchable
import android.content.Context import android.content.Context
import de.mm20.launcher2.appshortcuts.LauncherShortcutDeserializer 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.files.*
import de.mm20.launcher2.search.NullDeserializer import de.mm20.launcher2.search.NullDeserializer
import de.mm20.launcher2.search.NullSerializer import de.mm20.launcher2.search.NullSerializer
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer 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.WikipediaDeserializer
import de.mm20.launcher2.wikipedia.WikipediaSerializer 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 { internal fun getSerializer(searchable: Searchable?): SearchableSerializer {
if (searchable is LauncherApp) { if (searchable is LauncherApp) {

View File

@ -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.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer

View File

@ -40,7 +40,7 @@ dependencies {
implementation(libs.koin.android) implementation(libs.koin.android)
implementation(project(":data:favorites")) implementation(project(":data:searchable"))
implementation(project(":data:widgets")) implementation(project(":data:widgets"))
implementation(project(":data:search-actions")) implementation(project(":data:search-actions"))
implementation(project(":core:preferences")) implementation(project(":core:preferences"))

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.export import de.mm20.launcher2.preferences.export
import de.mm20.launcher2.preferences.import import de.mm20.launcher2.preferences.import
@ -21,7 +21,7 @@ import java.util.zip.ZipOutputStream
class BackupManager( class BackupManager(
private val context: Context, private val context: Context,
private val dataStore: LauncherDataStore, private val dataStore: LauncherDataStore,
private val favoritesRepository: FavoritesRepository, private val searchableRepository: SearchableRepository,
private val widgetRepository: WidgetRepository, private val widgetRepository: WidgetRepository,
private val searchActionRepository: SearchActionRepository, private val searchActionRepository: SearchActionRepository,
private val customAttrsRepository: CustomAttributesRepository, private val customAttrsRepository: CustomAttributesRepository,
@ -63,7 +63,7 @@ class BackupManager(
} }
if (include.contains(BackupComponent.Favorites)) { if (include.contains(BackupComponent.Favorites)) {
favoritesRepository.export(backupDir) searchableRepository.export(backupDir)
} }
if (include.contains(BackupComponent.Widgets)) { if (include.contains(BackupComponent.Widgets)) {
@ -104,7 +104,7 @@ class BackupManager(
} }
if (include.contains(BackupComponent.Favorites)) { if (include.contains(BackupComponent.Favorites)) {
favoritesRepository.import(restoreDir) searchableRepository.import(restoreDir)
} }
if (include.contains(BackupComponent.Widgets)) { if (include.contains(BackupComponent.Widgets)) {

1
services/favorites/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,46 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.services.favorites"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.koin.android)
implementation(project(":core:base"))
implementation(project(":core:i18n"))
implementation(project(":data:searchable"))
}

View File

21
services/favorites/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -0,0 +1,72 @@
package de.mm20.launcher2.services.favorites
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.searchable.SearchableRepository
import kotlinx.coroutines.flow.Flow
class FavoritesService(
val searchableRepository: SearchableRepository,
) {
fun getFavorites(
includeTypes: List<String>? = null,
excludeTypes: List<String>? = null,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
limit: Int = 100,
): Flow<List<SavableSearchable>> {
return searchableRepository.get(
hidden = false,
includeTypes = includeTypes,
excludeTypes = excludeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
limit = limit,
)
}
fun isPinned(searchable: SavableSearchable): Flow<Boolean> {
return searchableRepository.isPinned(searchable)
}
fun pinItem(searchable: SavableSearchable) {
searchableRepository.upsert(
searchable,
pinned = true,
hidden = false,
)
}
fun reset(searchable: SavableSearchable) {
searchableRepository.update(
searchable,
pinned = false,
hidden = false,
weight = 0.0,
launchCount = 0,
)
}
fun unpinItem(searchable: SavableSearchable) {
searchableRepository.upsert(
searchable,
pinned = false,
)
}
fun reportLaunch(searchable: SavableSearchable) {
searchableRepository.touch(searchable)
}
fun updateFavorites(
manuallySorted: List<SavableSearchable>,
automaticallySorted: List<SavableSearchable>
) {
searchableRepository.updateFavorites(
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted
)
}
}

View File

@ -0,0 +1,7 @@
package de.mm20.launcher2.services.favorites
import org.koin.dsl.module
val favoritesModule = module {
factory { FavoritesService(get()) }
}

View File

@ -48,6 +48,6 @@ dependencies {
implementation(project(":core:ktx")) implementation(project(":core:ktx"))
implementation(project(":core:crashreporter")) implementation(project(":core:crashreporter"))
implementation(project(":data:customattrs")) implementation(project(":data:customattrs"))
implementation(project(":data:favorites")) implementation(project(":data:searchable"))
} }

View File

@ -1,7 +1,7 @@
package de.mm20.launcher2.services.tags.impl package de.mm20.launcher2.services.tags.impl
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.services.tags.TagsService import de.mm20.launcher2.services.tags.TagsService
@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
internal class TagsServiceImpl( internal class TagsServiceImpl(
private val customAttributesRepository: CustomAttributesRepository, private val customAttributesRepository: CustomAttributesRepository,
private val favoritesRepository: FavoritesRepository, private val searchableRepository: SearchableRepository,
) : TagsService { ) : TagsService {
private val scope = CoroutineScope(Job() + Dispatchers.Default) private val scope = CoroutineScope(Job() + Dispatchers.Default)
override fun getAllTags(startsWith: String?): Flow<List<String>> { override fun getAllTags(startsWith: String?): Flow<List<String>> {
@ -22,7 +22,7 @@ internal class TagsServiceImpl(
} }
override fun deleteTag(tag: String) { override fun deleteTag(tag: String) {
favoritesRepository.remove(Tag(tag)) searchableRepository.delete(Tag(tag))
customAttributesRepository.deleteTag(tag) customAttributesRepository.deleteTag(tag)
} }
@ -44,15 +44,15 @@ internal class TagsServiceImpl(
} }
if (newName != null && newName != tag) { if (newName != null && newName != tag) {
customAttributesRepository.renameTag(tag, newName).join() customAttributesRepository.renameTag(tag, newName).join()
val pinnedTags = favoritesRepository.getFavorites( val pinnedTags = searchableRepository.get(
includeTypes = listOf(Tag.Domain), includeTypes = listOf(Tag.Domain),
manuallySorted = true, manuallySorted = true,
automaticallySorted = true automaticallySorted = true
).first() ).first()
val oldTag = Tag(tag) val oldTag = Tag(tag)
if (pinnedTags.any { it.key == oldTag.key }) { if (pinnedTags.any { it.key == oldTag.key }) {
favoritesRepository.unpinItem(oldTag) searchableRepository.update(oldTag, pinned = false)
favoritesRepository.pinItem(Tag(newName)) searchableRepository.update(Tag(newName), pinned = true)
} }
} }

View File

@ -283,7 +283,7 @@ include(":data:widgets")
include(":data:weather") include(":data:weather")
include(":data:notifications") include(":data:notifications")
include(":data:search-actions") include(":data:search-actions")
include(":data:favorites") include(":data:searchable")
include(":services:accounts") include(":services:accounts")
include(":services:tags") include(":services:tags")
@ -301,3 +301,4 @@ include(":libs:g-services")
include(":libs:ms-services") include(":libs:ms-services")
include(":services:global-actions") include(":services:global-actions")
include(":services:widgets") include(":services:widgets")
include(":services:favorites")