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(":data:currencies"))
implementation(project(":data:customattrs"))
implementation(project(":data:favorites"))
implementation(project(":data:searchable"))
implementation(project(":data:files"))
implementation(project(":libs:g-services"))
implementation(project(":core:i18n"))
@ -157,6 +157,7 @@ dependencies {
implementation(project(":data:search-actions"))
implementation(project(":services:global-actions"))
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
//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.contacts.contactsModule
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.icons.iconsModule
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.preferences.preferencesModule
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.widgets.widgetsServiceModule
import de.mm20.launcher2.weather.weatherModule
@ -67,6 +68,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
customAttrsModule,
databaseModule,
favoritesModule,
searchableModule,
filesModule,
globalActionsModule,
iconsModule,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.util.Log
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.AppShortcut
@ -37,6 +36,6 @@ class ShortcutItemVM(private val shortcut: AppShortcut) : SearchableItemVM(short
fun deleteShortcut() {
if (!canDelete) return
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.BadgeRepository
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.LauncherIcon
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.Searchable
import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
@ -34,7 +35,7 @@ import org.koin.core.component.inject
class EditFavoritesSheetVM : ViewModel(), KoinComponent {
private val repository: FavoritesRepository by inject()
private val favoritesService: FavoritesService by inject()
private val shortcutRepository: AppShortcutRepository by inject()
private val iconRepository: IconRepository by inject()
private val badgeRepository: BadgeRepository by inject()
@ -58,19 +59,19 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
suspend fun reload(showLoadingIndicator: Boolean = true) {
loading.value = showLoadingIndicator
manuallySorted = mutableListOf()
manuallySorted = repository.getFavorites(
manuallySorted = favoritesService.getFavorites(
manuallySorted = true,
excludeTypes = listOf("tag"),
).first().toMutableList()
automaticallySorted = repository.getFavorites(
automaticallySorted = favoritesService.getFavorites(
automaticallySorted = true,
excludeTypes = listOf("tag"),
).first().toMutableList()
frequentlyUsed = repository.getFavorites(
frequentlyUsed = favoritesService.getFavorites(
frequentlyUsed = true,
excludeTypes = listOf("tag"),
).first().toMutableList()
val pinnedTags = repository.getFavorites(
val pinnedTags = favoritesService.getFavorites(
includeTypes = listOf("tag"),
manuallySorted = true,
automaticallySorted = true,
@ -169,7 +170,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
}
private fun save() {
repository.updateFavorites(
favoritesService.updateFavorites(
manuallySorted = buildList {
pinnedTags.value?.let { addAll(it) }
addAll(manuallySorted)
@ -236,7 +237,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
val item =
gridItems.find { it is FavoritesSheetGridItem.Favorite && it.item.key == key } as FavoritesSheetGridItem.Favorite?
if (item != null) {
repository.removeFromFavorites(item.item)
favoritesService.reset(item.item)
automaticallySorted.removeAll { it.key == item.item.key }
|| manuallySorted.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 }
buildItemList()
customAttributesRepository.addTag(item.item, tag)
repository.unpinItem(item.item)
favoritesService.unpinItem(item.item)
viewModelScope.launch {
frequentlyUsed = repository.getFavorites(
frequentlyUsed = favoritesService.getFavorites(
frequentlyUsed = true,
excludeTypes = listOf("tag"),
).first().toMutableList()

View File

@ -10,12 +10,13 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
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.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
@ -31,11 +32,12 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore 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 pinnedCalendarEvents =
favoritesRepository.getFavorites(
favoritesService.getFavorites(
includeTypes = listOf(CalendarEvent.Domain),
automaticallySorted = true,
manuallySorted = true,
@ -158,7 +160,10 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
excludeAllDayEvents = settings.hideAlldayEvents,
excludeCalendars = settings.excludeCalendarsList
).collectLatest { events ->
favoritesRepository.getHiddenCalendarEventKeys().collectLatest { hidden ->
searchableRepository.getKeys(
includeTypes = listOf(CalendarEvent.Domain),
hidden = true,
).collectLatest { hidden ->
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.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
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.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.WidgetRepository
@ -26,7 +26,7 @@ import org.koin.core.component.inject
class FavoritesPartProvider : PartProvider, KoinComponent {
private val favoritesRepository: FavoritesRepository by inject()
private val favoritesService: FavoritesService by inject()
private val widgetRepository: WidgetRepository by inject()
private val dataStore: LauncherDataStore by inject()
@ -47,7 +47,7 @@ class FavoritesPartProvider : PartProvider, KoinComponent {
)
val favorites by remember(columns, excludeCalendar, layout) {
favoritesRepository.getFavorites(
favoritesService.getFavorites(
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
manuallySorted = true,
automaticallySorted = true,

View File

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

View File

@ -4,7 +4,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
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.LauncherIcon
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
@ -26,7 +24,7 @@ import org.koin.core.component.inject
class GestureSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore 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()
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 }
.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)
fun setSwipeLeftApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { favoritesRepository.save(it) }
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()
@ -106,13 +104,13 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val swipeRightApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeRightApp }
.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)
fun setSwipeRightApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { favoritesRepository.save(it) }
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()
@ -126,13 +124,13 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val swipeDownApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.swipeDownApp }
.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)
fun setSwipeDownApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { favoritesRepository.save(it) }
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()
@ -146,13 +144,13 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val longPressApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.longPressApp }
.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)
fun setLongPressApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { favoritesRepository.save(it) }
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()
@ -166,13 +164,13 @@ class GestureSettingsScreenVM : ViewModel(), KoinComponent {
val doubleTapApp: Flow<SavableSearchable?> = dataStore.data.map { it.gestures.doubleTapApp }
.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)
fun setDoubleTapApp(searchable: SavableSearchable?) {
viewModelScope.launch {
searchable?.let { favoritesRepository.save(it) }
searchable?.let { searchableRepository.insert(it) }
dataStore.updateData {
it.toBuilder()
.setGestures(it.gestures.toBuilder()

View File

@ -10,7 +10,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
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.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel
@ -26,26 +26,26 @@ import org.koin.core.component.inject
class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
private val appRepository: AppRepository by inject()
private val favoritesRepository: FavoritesRepository by inject()
private val searchableRepository: SearchableRepository by inject()
private val iconRepository: IconRepository by inject()
val allApps = appRepository.getAllInstalledApps().map {
withContext(Dispatchers.Default) { it.sorted() }
}.asLiveData()
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)
}
fun isHidden(searchable: SavableSearchable): Flow<Boolean> {
return favoritesRepository.isHidden(searchable)
return searchableRepository.isHidden(searchable)
}
fun setHidden(searchable: SavableSearchable, hidden: Boolean) {
if(hidden) {
favoritesRepository.hideItem(searchable)
searchableRepository.upsert(searchable, hidden = true, pinned = false)
} 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_21_22
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_7_8
import de.mm20.launcher2.database.migrations.Migration_8_9
@ -39,14 +40,15 @@ import java.util.UUID
WidgetEntity::class,
CustomAttributeEntity::class,
SearchActionEntity::class
], version = 23, exportSchema = true
], version = 24, exportSchema = true
)
@TypeConverters(ComponentNameConverter::class, StringListConverter::class)
@TypeConverters(ComponentNameConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun weatherDao(): WeatherDao
abstract fun searchDao(): SearchDao
abstract fun iconDao(): IconDao
abstract fun searchableDao(): SearchableDao
abstract fun widgetDao(): WidgetDao
abstract fun currencyDao(): CurrencyDao
abstract fun backupDao(): BackupRestoreDao
@ -114,6 +116,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration_20_21(),
Migration_21_22(),
Migration_22_23(),
Migration_23_24(),
).build()
if (_instance == null) _instance = 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,
val type: String,
@ColumnInfo(name = "searchable") val serializedSearchable: String,
var launchCount: Int,
@ColumnInfo(name = "pinned") var pinPosition: Int,
var hidden: Boolean,
@ColumnInfo(defaultValue = "0.0") var weight: Double
@ColumnInfo(defaultValue = "0") val launchCount: Int,
@ColumnInfo(defaultValue = "0") val pinPosition: Int,
@ColumnInfo(defaultValue = "0") val hidden: Boolean,
@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:ktx"))
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.database.AppDatabase
import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SavableSearchable
import kotlinx.collections.immutable.ImmutableList
@ -46,7 +46,7 @@ interface CustomAttributesRepository {
internal class CustomAttributesRepositoryImpl(
private val appDatabase: AppDatabase,
private val favoritesRepository: FavoritesRepository
private val searchableRepository: SearchableRepository
) : CustomAttributesRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
@ -79,7 +79,7 @@ internal class CustomAttributesRepositoryImpl(
override fun setCustomLabel(searchable: SavableSearchable, label: String) {
val dao = appDatabase.customAttrsDao()
scope.launch {
favoritesRepository.save(searchable)
searchableRepository.insert(searchable)
appDatabase.runInTransaction {
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
dao.setCustomAttribute(
@ -102,7 +102,7 @@ internal class CustomAttributesRepositoryImpl(
override fun setTags(searchable: SavableSearchable, tags: List<String>) {
val dao = appDatabase.customAttrsDao()
scope.launch {
favoritesRepository.save(searchable)
searchableRepository.insert(searchable)
dao.setTags(searchable.key, tags.map {
CustomTag(it).toDatabaseEntity(searchable.key)
})
@ -128,7 +128,7 @@ internal class CustomAttributesRepositoryImpl(
override fun getItemsForTag(tag: String): Flow<List<SavableSearchable>> {
val dao = appDatabase.customAttrsDao()
return dao.getItemsWithTag(tag).map {
favoritesRepository.getFromKeys(it)
searchableRepository.getByKeys(it)
}
}
@ -137,7 +137,7 @@ internal class CustomAttributesRepositoryImpl(
return scope.launch {
dao.setItemsWithTag(tag, items.map { it.key })
for (item in items) {
favoritesRepository.save(item)
searchableRepository.insert(item)
}
}
}
@ -172,7 +172,7 @@ internal class CustomAttributesRepositoryImpl(
}
val dao = appDatabase.customAttrsDao()
return dao.search("%$query%").map {
favoritesRepository.getFromKeys(it).toImmutableList()
searchableRepository.getByKeys(it).toImmutableList()
}
}

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.search.SavableSearchable

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.favorites
package de.mm20.launcher2.searchable
data class SavedSearchableRankInfo(
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.util.Log
import androidx.room.withTransaction
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.SavedSearchableEntity
import de.mm20.launcher2.database.entities.SavedSearchableUpdatePinEntity
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.WeightFactor
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONException
import org.koin.core.component.KoinComponent
import java.io.File
interface FavoritesRepository {
interface SearchableRepository {
fun insert(
searchable: SavableSearchable,
)
fun upsert(
searchable: SavableSearchable,
hidden: Boolean? = null,
pinned: Boolean? = null,
launchCount: Int? = null,
weight: Double? = null,
)
fun update(
searchable: SavableSearchable,
hidden: Boolean? = null,
pinned: Boolean? = null,
launchCount: Int? = null,
weight: Double? = null,
)
/**
* Get favorites
* @param includeTypes Include only items of these types. Cannot be used together with excludeTypes.
* @param excludeTypes Exclude only items of these types. Cannot be used together with includeTypes.
* @param manuallySorted Include items that have been sorted manually
* @param automaticallySorted Include items that are pinned but not sorted
* @param frequentlyUsed Include items that are not pinned but most frequently used
* @param limit Maximum number of items returned.
*/
fun getFavorites(
* Touch a searchable to update its weight and launch counter
**/
fun touch(
searchable: SavableSearchable,
)
fun get(
includeTypes: List<String>? = null,
excludeTypes: List<String>? = null,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
limit: Int = 100
hidden: Boolean = false,
limit: Int = 100,
): Flow<List<SavableSearchable>>
fun getKeys(
includeTypes: List<String>? = null,
excludeTypes: List<String>? = null,
manuallySorted: Boolean = false,
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
hidden: Boolean = false,
limit: Int = 100,
): Flow<List<String>>
fun getHiddenCalendarEventKeys(): Flow<List<String>>
fun isPinned(searchable: SavableSearchable): Flow<Boolean>
fun pinItem(searchable: SavableSearchable)
fun unpinItem(searchable: SavableSearchable)
fun isHidden(searchable: SavableSearchable): Flow<Boolean>
fun hideItem(searchable: SavableSearchable)
fun unhideItem(searchable: SavableSearchable)
fun incrementLaunchCounter(searchable: SavableSearchable)
fun updateFavorites(
manuallySorted: List<SavableSearchable>,
automaticallySorted: List<SavableSearchable>,
)
fun getHiddenItems(): Flow<List<SavableSearchable>>
fun getHiddenItemKeys(): Flow<List<String>>
/**
* Returns the given keys sorted by relevance.
* The first item in the list is the most relevant.
@ -65,24 +94,13 @@ interface FavoritesRepository {
/**
* Remove this item from the Searchable database
*/
fun remove(searchable: SavableSearchable)
/**
* Remove this item from favorites and reset launch counter
*/
fun removeFromFavorites(searchable: SavableSearchable)
/**
* Ensure that this searchable exists in the Favorites table.
* If it doesn't exist, insert it with 0 launch count, not pinned and not hidden
*/
fun save(searchable: SavableSearchable)
fun delete(searchable: SavableSearchable)
/**
* Get items with the given keys from the favorites database.
* Items that don't exist in the database will not be returned.
*/
suspend fun getFromKeys(keys: List<String>): List<SavableSearchable>
suspend fun getByKeys(keys: List<String>): List<SavableSearchable>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
@ -95,177 +113,193 @@ interface FavoritesRepository {
suspend fun cleanupDatabase(): Int
}
internal class FavoritesRepositoryImpl(
internal class SearchableRepositoryImpl(
private val context: Context,
private val database: AppDatabase,
private val dataStore: LauncherDataStore
) : FavoritesRepository, KoinComponent {
) : SearchableRepository, KoinComponent {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
override fun getFavorites(
override fun insert(searchable: SavableSearchable) {
val dao = database.searchableDao()
scope.launch {
dao.insert(
SavedSearchableEntity(
key = searchable.key,
type = searchable.domain,
serializedSearchable = searchable.serialize() ?: return@launch,
hidden = false,
launchCount = 0,
weight = 0.0,
pinPosition = 0,
)
)
}
}
override fun upsert(
searchable: SavableSearchable,
hidden: Boolean?,
pinned: Boolean?,
launchCount: Int?,
weight: Double?
) {
val dao = database.searchableDao()
scope.launch {
val entity = dao.getByKey(searchable.key).firstOrNull()
dao.upsert(
SavedSearchableEntity(
key = searchable.key,
type = searchable.domain,
hidden = hidden ?: entity?.hidden ?: false,
pinPosition = pinned?.let { if (it) 1 else 0 } ?: entity?.pinPosition ?: 0,
launchCount = launchCount ?: entity?.launchCount ?: 0,
weight = weight ?: entity?.weight ?: 0.0,
serializedSearchable = searchable.serialize() ?: return@launch,
)
)
}
}
override fun update(
searchable: SavableSearchable,
hidden: Boolean?,
pinned: Boolean?,
launchCount: Int?,
weight: Double?
) {
val dao = database.searchableDao()
scope.launch {
val entity = dao.getByKey(searchable.key).firstOrNull()
dao.upsert(
SavedSearchableEntity(
key = searchable.key,
type = searchable.domain,
hidden = hidden ?: entity?.hidden ?: false,
pinPosition = pinned?.let { if (it) 1 else 0 } ?: entity?.pinPosition ?: 0,
launchCount = launchCount ?: entity?.launchCount ?: 0,
weight = weight ?: entity?.weight ?: 0.0,
serializedSearchable = searchable.serialize() ?: return@launch,
)
)
}
}
override fun touch(searchable: SavableSearchable) {
scope.launch {
val weightFactor =
when (dataStore.data.map { it.resultOrdering.weightFactor }.firstOrNull()) {
WeightFactor.Low -> WEIGHT_FACTOR_LOW
WeightFactor.High -> WEIGHT_FACTOR_HIGH
else -> WEIGHT_FACTOR_MEDIUM
}
val item = SavedSearchable(searchable.key, searchable, 0, 0, false, 0.0)
item.toDatabaseEntity()?.let {
database.searchableDao()
.touch(it, weightFactor)
}
}
}
override fun get(
includeTypes: List<String>?,
excludeTypes: List<String>?,
manuallySorted: Boolean,
automaticallySorted: Boolean,
frequentlyUsed: Boolean,
hidden: Boolean,
limit: Int
): Flow<List<SavableSearchable>> {
val dao = database.searchDao()
val dao = database.searchableDao()
val entities = when {
includeTypes == null && excludeTypes == null -> dao.getFavorites(
includeTypes == null && excludeTypes == null -> dao.get(
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
includeTypes != null && excludeTypes == null -> {
dao.getFavoritesWithTypes(
includeTypes = includeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
limit = limit
)
}
includeTypes == null -> dao.getExcludeTypes(
excludeTypes = excludeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
excludeTypes != null && includeTypes == null -> {
dao.getFavoritesWithoutTypes(
excludeTypes = excludeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
limit = limit
)
}
excludeTypes == null -> dao.getIncludeTypes(
includeTypes = includeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
else -> throw IllegalArgumentException("You can either use includeTypes or excludeTypes, not both")
else -> throw IllegalArgumentException("Cannot specify both includeTypes and excludeTypes")
}
return entities.map {
it.mapNotNull { fromDatabaseEntity(it).searchable }
}
}
override fun getHiddenCalendarEventKeys(): Flow<List<String>> {
return database.searchDao().getHiddenCalendarEventKeys()
override fun getKeys(
includeTypes: List<String>?,
excludeTypes: List<String>?,
manuallySorted: Boolean,
automaticallySorted: Boolean,
frequentlyUsed: Boolean,
hidden: Boolean,
limit: Int
): Flow<List<String>> {
val dao = database.searchableDao()
return when {
includeTypes == null && excludeTypes == null -> dao.getKeys(
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
includeTypes == null -> dao.getKeysExcludeTypes(
excludeTypes = excludeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
excludeTypes == null -> dao.getKeysIncludeTypes(
includeTypes = includeTypes,
manuallySorted = manuallySorted,
automaticallySorted = automaticallySorted,
frequentlyUsed = frequentlyUsed,
hidden = hidden,
limit = limit
)
else -> throw IllegalArgumentException("Cannot specify both includeTypes and excludeTypes")
}
}
override fun isPinned(searchable: SavableSearchable): Flow<Boolean> {
return database.searchDao().isPinned(searchable.key)
}
override fun pinItem(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val dao = database.searchDao()
val databaseItem = dao.getFavorite(searchable.key)
val savedSearchable = SavedSearchable(
key = searchable.key,
searchable = searchable,
launchCount = databaseItem?.launchCount ?: 0,
pinPosition = 1,
hidden = false,
weight = databaseItem?.weight ?: 0.0
)
savedSearchable.toDatabaseEntity()?.let { dao.insertReplaceExisting(it) }
}
}
}
override fun unpinItem(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
database.searchDao().unpinFavorite(searchable.key)
}
}
return database.searchableDao().isPinned(searchable.key)
}
override fun isHidden(searchable: SavableSearchable): Flow<Boolean> {
return database.searchDao().isHidden(searchable.key)
return database.searchableDao().isHidden(searchable.key)
}
override fun hideItem(searchable: SavableSearchable) {
override fun delete(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val dao = database.searchDao()
val databaseItem = dao.getFavorite(searchable.key)
val savedSearchable = SavedSearchable(
key = searchable.key,
searchable = searchable,
launchCount = databaseItem?.launchCount ?: 0,
pinPosition = 0,
hidden = true,
weight = databaseItem?.weight ?: 0.0
)
savedSearchable.toDatabaseEntity()?.let { dao.insertReplaceExisting(it) }
}
}
}
override fun unhideItem(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
database.searchDao().unhideItem(searchable.key)
}
}
}
override fun incrementLaunchCounter(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val weightFactor =
when (dataStore.data.map { it.resultOrdering.weightFactor }.firstOrNull()) {
WeightFactor.Low -> WEIGHT_FACTOR_LOW
WeightFactor.High -> WEIGHT_FACTOR_HIGH
else -> WEIGHT_FACTOR_MEDIUM
}
val item = SavedSearchable(searchable.key, searchable, 0, 0, false, 0.0)
item.toDatabaseEntity()?.let {
database.searchDao()
.incrementLaunchCount(it, weightFactor)
}
}
}
}
override fun getHiddenItems(): Flow<List<SavableSearchable>> {
return database.searchDao().getHiddenItems().map {
it.mapNotNull { fromDatabaseEntity(it).searchable }
}
}
override fun getHiddenItemKeys(): Flow<List<String>> {
return database.searchDao().getHiddenItemKeys()
}
override fun remove(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
database.searchDao().deleteByKey(searchable.key)
}
}
}
override fun removeFromFavorites(searchable: SavableSearchable) {
scope.launch {
database.searchDao().resetPinStatusAndLaunchCounter(searchable.key)
}
}
override fun save(searchable: SavableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val entity = SavedSearchable(
key = searchable.key,
searchable = searchable,
launchCount = 0,
pinPosition = 0,
hidden = false,
weight = 0.0
).toDatabaseEntity() ?: return@withContext
database.searchDao().insertSkipExisting(entity)
}
database.searchableDao().delete(searchable.key)
}
}
@ -273,51 +307,42 @@ internal class FavoritesRepositoryImpl(
manuallySorted: List<SavableSearchable>,
automaticallySorted: List<SavableSearchable>
) {
val dao = database.searchDao()
val dao = database.searchableDao()
scope.launch {
withContext(Dispatchers.IO) {
val keys = manuallySorted.map { it.key } + automaticallySorted.map { it.key }
val entities = dao.getFromKeys(keys)
val updatedManuallySorted = manuallySorted.mapIndexedNotNull { index, searchable ->
val entity = entities.find { searchable.key == it.key } ?: SavedSearchable(
key = searchable.key,
searchable = searchable,
launchCount = 0,
pinPosition = 0,
hidden = false,
weight = 0.0
).toDatabaseEntity() ?: return@mapIndexedNotNull null
entity.pinPosition = manuallySorted.size - index + 1
entity
}
val updatedAutomaticallySorted =
automaticallySorted.mapIndexedNotNull { index, searchable ->
val entity = entities.find { searchable.key == it.key } ?: SavedSearchable(
key = searchable.key,
searchable = searchable,
launchCount = 0,
pinPosition = 0,
hidden = false,
weight = 0.0
).toDatabaseEntity() ?: return@mapIndexedNotNull null
entity.pinPosition = 1
entity
database.withTransaction {
dao.unpinAll()
dao.upsert(
manuallySorted.mapIndexedNotNull { index, savableSearchable ->
SavedSearchableUpdatePinEntity(
key = savableSearchable.key,
type = savableSearchable.domain,
pinPosition = manuallySorted.size - index + 1,
serializedSearchable = savableSearchable.serialize()
?: return@mapIndexedNotNull null,
)
}
database.runInTransaction {
dao.unpinAll()
dao.insertAllReplaceExisting(updatedManuallySorted)
dao.insertAllReplaceExisting(updatedAutomaticallySorted)
}
)
dao.upsert(
automaticallySorted.mapNotNull { savableSearchable ->
SavedSearchableUpdatePinEntity(
key = savableSearchable.key,
type = savableSearchable.domain,
pinPosition = 1,
serializedSearchable = savableSearchable.serialize()
?: return@mapNotNull null,
)
}
)
}
}
}
override fun sortByRelevance(keys: List<String>): Flow<List<String>> {
return database.searchDao().sortByRelevance(keys)
return database.searchableDao().sortByRelevance(keys)
}
override fun sortByWeight(keys: List<String>): Flow<List<String>> {
return database.searchDao().sortByWeight(keys)
return database.searchableDao().sortByWeight(keys)
}
private fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable {
@ -337,13 +362,13 @@ internal class FavoritesRepositoryImpl(
private fun removeInvalidItem(key: String) {
scope.launch {
database.searchDao().deleteByKey(key)
database.searchableDao().delete(key)
}
}
override suspend fun getFromKeys(keys: List<String>): List<SavableSearchable> {
val dao = database.searchDao()
return dao.getFromKeys(keys)
override suspend fun getByKeys(keys: List<String>): List<SavableSearchable> {
val dao = database.searchableDao()
return dao.getByKeys(keys)
.mapNotNull { fromDatabaseEntity(it).searchable }
}

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.favorites
package de.mm20.launcher2.searchable
import android.content.Context
import de.mm20.launcher2.appshortcuts.LauncherShortcutDeserializer
@ -12,6 +12,7 @@ import de.mm20.launcher2.contacts.ContactSerializer
import de.mm20.launcher2.files.*
import de.mm20.launcher2.search.NullDeserializer
import de.mm20.launcher2.search.NullSerializer
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
@ -21,6 +22,10 @@ import de.mm20.launcher2.websites.WebsiteSerializer
import de.mm20.launcher2.wikipedia.WikipediaDeserializer
import de.mm20.launcher2.wikipedia.WikipediaSerializer
internal fun SavableSearchable.serialize(): String? {
val serializer = getSerializer(this)
return serializer.serialize(this)
}
internal fun getSerializer(searchable: Searchable?): SearchableSerializer {
if (searchable is LauncherApp) {

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.SearchableDeserializer

View File

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

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.net.Uri
import android.os.Build
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.export
import de.mm20.launcher2.preferences.import
@ -21,7 +21,7 @@ import java.util.zip.ZipOutputStream
class BackupManager(
private val context: Context,
private val dataStore: LauncherDataStore,
private val favoritesRepository: FavoritesRepository,
private val searchableRepository: SearchableRepository,
private val widgetRepository: WidgetRepository,
private val searchActionRepository: SearchActionRepository,
private val customAttrsRepository: CustomAttributesRepository,
@ -63,7 +63,7 @@ class BackupManager(
}
if (include.contains(BackupComponent.Favorites)) {
favoritesRepository.export(backupDir)
searchableRepository.export(backupDir)
}
if (include.contains(BackupComponent.Widgets)) {
@ -104,7 +104,7 @@ class BackupManager(
}
if (include.contains(BackupComponent.Favorites)) {
favoritesRepository.import(restoreDir)
searchableRepository.import(restoreDir)
}
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:crashreporter"))
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
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.data.Tag
import de.mm20.launcher2.services.tags.TagsService
@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
internal class TagsServiceImpl(
private val customAttributesRepository: CustomAttributesRepository,
private val favoritesRepository: FavoritesRepository,
private val searchableRepository: SearchableRepository,
) : TagsService {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
override fun getAllTags(startsWith: String?): Flow<List<String>> {
@ -22,7 +22,7 @@ internal class TagsServiceImpl(
}
override fun deleteTag(tag: String) {
favoritesRepository.remove(Tag(tag))
searchableRepository.delete(Tag(tag))
customAttributesRepository.deleteTag(tag)
}
@ -44,15 +44,15 @@ internal class TagsServiceImpl(
}
if (newName != null && newName != tag) {
customAttributesRepository.renameTag(tag, newName).join()
val pinnedTags = favoritesRepository.getFavorites(
val pinnedTags = searchableRepository.get(
includeTypes = listOf(Tag.Domain),
manuallySorted = true,
automaticallySorted = true
).first()
val oldTag = Tag(tag)
if (pinnedTags.any { it.key == oldTag.key }) {
favoritesRepository.unpinItem(oldTag)
favoritesRepository.pinItem(Tag(newName))
searchableRepository.update(oldTag, pinned = false)
searchableRepository.update(Tag(newName), pinned = true)
}
}

View File

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