diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 0d5b21f7..f2b6d296 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -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) diff --git a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt index f59d2b39..32f85766 100644 --- a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt +++ b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt @@ -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, diff --git a/app/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt b/app/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt index d8920a77..09f99245 100644 --- a/app/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt +++ b/app/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt @@ -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() } diff --git a/app/ui/build.gradle.kts b/app/ui/build.gradle.kts index 57c1b311..c0b961cb 100644 --- a/app/ui/build.gradle.kts +++ b/app/ui/build.gradle.kts @@ -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")) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt index 88c018cf..e1b401a4 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt @@ -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 - 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, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt index 09e9d9ec..f7264756 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt @@ -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, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index 4e798c87..7889760b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -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(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() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt index 62b875c5..0ab03661 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt @@ -120,15 +120,15 @@ class AppItemVM( } fun isShortcutPinned(shortcut: AppShortcut): Flow { - 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) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt index c2141017..7a6e5f7f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/SearchableItemVM.kt @@ -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 } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt index 39aa8334..009b7140 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItemVM.kt @@ -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) } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheetVM.kt index b2e46281..af83472c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheetVM.kt @@ -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() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt index b69fd021..e6578ef7 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt @@ -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>(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) } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/FavoritesPartProvider.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/FavoritesPartProvider.kt index 4bd0ffa1..60a75876 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/FavoritesPartProvider.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/FavoritesPartProvider.kt @@ -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, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreenVM.kt index 2e1c0663..e4c67ac1 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreenVM.kt @@ -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 } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt index d7dcf597..48b00d73 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreenVM.kt @@ -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 = 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 = 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 = 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 = 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 = 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() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/hiddenitems/HiddenItemsSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/hiddenitems/HiddenItemsSettingsScreenVM.kt index 798a7d7a..4afef22f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/hiddenitems/HiddenItemsSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/hiddenitems/HiddenItemsSettingsScreenVM.kt @@ -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> = 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 { - 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) } } diff --git a/core/database/schemas/de.mm20.launcher2.database.AppDatabase/24.json b/core/database/schemas/de.mm20.launcher2.database.AppDatabase/24.json new file mode 100644 index 00000000..6cdecbee --- /dev/null +++ b/core/database/schemas/de.mm20.launcher2.database.AppDatabase/24.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt b/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt index 7bb9ab6e..d11d7496 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt @@ -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 diff --git a/core/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt b/core/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt deleted file mode 100644 index 355b76e5..00000000 --- a/core/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt +++ /dev/null @@ -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) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - fun insertAllSkipExisting(items: List) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - fun insertSkipExisting(items: SavedSearchableEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertAllReplaceExisting(items: List) - - @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> - - @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, - manuallySorted: Boolean = false, - automaticallySorted: Boolean = false, - frequentlyUsed: Boolean = false, - limit: Int, - ): Flow> - - @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, - manuallySorted: Boolean = false, - automaticallySorted: Boolean = false, - frequentlyUsed: Boolean = false, - limit: Int, - ): Flow> - - @Query("SELECT `key` FROM Searchable WHERE hidden = 1 AND type = 'calendar'") - fun getHiddenCalendarEventKeys(): Flow> - - @Query("DELETE FROM Searchable WHERE `key` IN (:keys)") - fun deleteAll(keys: List) - - @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 - - - @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 - - @Query("SELECT `key` FROM SEARCHABLE WHERE hidden = 1") - fun getHiddenItemKeys(): Flow> - - @Query("SELECT * FROM SEARCHABLE WHERE hidden = 1") - fun getHiddenItems(): Flow> - - @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): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertReplaceExisting(toDatabaseEntity: SavedSearchableEntity) - - @Transaction - fun saveFavorites(favorites: List) { - 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): Flow> - - @Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) ORDER BY `weight` DESC, pinned DESC") - fun sortByWeight(keys: List): Flow> - - @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) -} diff --git a/core/database/src/main/java/de/mm20/launcher2/database/SearchableDao.kt b/core/database/src/main/java/de/mm20/launcher2/database/SearchableDao.kt new file mode 100644 index 00000000..26a33010 --- /dev/null +++ b/core/database/src/main/java/de/mm20/launcher2/database/SearchableDao.kt @@ -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) + + @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> + + @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?, + manuallySorted: Boolean = false, + automaticallySorted: Boolean = false, + frequentlyUsed: Boolean = false, + hidden: Boolean = false, + limit: Int, + ): Flow> + + @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?, + manuallySorted: Boolean = false, + automaticallySorted: Boolean = false, + frequentlyUsed: Boolean = false, + hidden: Boolean = false, + limit: Int, + ): Flow> + + @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> + + @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?, + manuallySorted: Boolean = false, + automaticallySorted: Boolean = false, + frequentlyUsed: Boolean = false, + hidden: Boolean = false, + limit: Int, + ): Flow> + + @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?, + manuallySorted: Boolean = false, + automaticallySorted: Boolean = false, + frequentlyUsed: Boolean = false, + hidden: Boolean = false, + limit: Int, + ): Flow> + + @Query("SELECT * FROM Searchable WHERE `key` IN (:keys)") + suspend fun getByKeys(keys: List): List + + @Query("SELECT * FROM Searchable WHERE `key` = :key") + fun getByKey(key: String): Flow + + @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): Flow> + + @Query("SELECT `key` FROM Searchable WHERE `key` IN (:keys) ORDER BY `weight` DESC, pinPosition DESC") + fun sortByWeight(keys: List): Flow> + + @Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1") + fun isHidden(key: String): Flow + + @Query("SELECT pinPosition FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinPosition ORDER BY pinPosition DESC LIMIT 1") + fun isPinned(key: String): Flow +} \ No newline at end of file diff --git a/core/database/src/main/java/de/mm20/launcher2/database/entities/SavedSearchableEntity.kt b/core/database/src/main/java/de/mm20/launcher2/database/entities/SavedSearchableEntity.kt index 21bb5895..afe95a69 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/entities/SavedSearchableEntity.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/entities/SavedSearchableEntity.kt @@ -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, +) \ No newline at end of file diff --git a/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_23_24.kt b/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_23_24.kt new file mode 100644 index 00000000..5e6ac049 --- /dev/null +++ b/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_23_24.kt @@ -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") + } +} \ No newline at end of file diff --git a/data/customattrs/build.gradle.kts b/data/customattrs/build.gradle.kts index dfee48f2..a084efae 100644 --- a/data/customattrs/build.gradle.kts +++ b/data/customattrs/build.gradle.kts @@ -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")) } \ No newline at end of file diff --git a/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttributesRepository.kt b/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttributesRepository.kt index e8dac3ed..7e4542b3 100644 --- a/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttributesRepository.kt +++ b/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttributesRepository.kt @@ -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) { 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> { 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() } } diff --git a/data/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt b/data/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt deleted file mode 100644 index 4e5e57f4..00000000 --- a/data/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt +++ /dev/null @@ -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 { FavoritesRepositoryImpl(androidContext(), get(), get()) } -} \ No newline at end of file diff --git a/data/favorites/.gitignore b/data/searchable/.gitignore similarity index 100% rename from data/favorites/.gitignore rename to data/searchable/.gitignore diff --git a/data/favorites/build.gradle.kts b/data/searchable/build.gradle.kts similarity index 100% rename from data/favorites/build.gradle.kts rename to data/searchable/build.gradle.kts diff --git a/data/favorites/consumer-rules.pro b/data/searchable/consumer-rules.pro similarity index 100% rename from data/favorites/consumer-rules.pro rename to data/searchable/consumer-rules.pro diff --git a/data/favorites/proguard-rules.pro b/data/searchable/proguard-rules.pro similarity index 100% rename from data/favorites/proguard-rules.pro rename to data/searchable/proguard-rules.pro diff --git a/data/favorites/src/main/AndroidManifest.xml b/data/searchable/src/main/AndroidManifest.xml similarity index 100% rename from data/favorites/src/main/AndroidManifest.xml rename to data/searchable/src/main/AndroidManifest.xml diff --git a/data/favorites/src/main/java/de/mm20/launcher2/search/data/Tag.kt b/data/searchable/src/main/java/de/mm20/launcher2/search/data/Tag.kt similarity index 100% rename from data/favorites/src/main/java/de/mm20/launcher2/search/data/Tag.kt rename to data/searchable/src/main/java/de/mm20/launcher2/search/data/Tag.kt diff --git a/data/searchable/src/main/java/de/mm20/launcher2/searchable/Module.kt b/data/searchable/src/main/java/de/mm20/launcher2/searchable/Module.kt new file mode 100644 index 00000000..2ecfc429 --- /dev/null +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/Module.kt @@ -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 { SearchableRepositoryImpl(androidContext(), get(), get()) } +} \ No newline at end of file diff --git a/data/favorites/src/main/java/de/mm20/launcher2/favorites/SavedSearchable.kt b/data/searchable/src/main/java/de/mm20/launcher2/searchable/SavedSearchable.kt similarity index 96% rename from data/favorites/src/main/java/de/mm20/launcher2/favorites/SavedSearchable.kt rename to data/searchable/src/main/java/de/mm20/launcher2/searchable/SavedSearchable.kt index 71461a41..41551a56 100644 --- a/data/favorites/src/main/java/de/mm20/launcher2/favorites/SavedSearchable.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/SavedSearchable.kt @@ -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 diff --git a/data/favorites/src/main/java/de/mm20/launcher2/favorites/SavedSearchableRankInfo.kt b/data/searchable/src/main/java/de/mm20/launcher2/searchable/SavedSearchableRankInfo.kt similarity index 74% rename from data/favorites/src/main/java/de/mm20/launcher2/favorites/SavedSearchableRankInfo.kt rename to data/searchable/src/main/java/de/mm20/launcher2/searchable/SavedSearchableRankInfo.kt index fec43207..bfbdda7c 100644 --- a/data/favorites/src/main/java/de/mm20/launcher2/favorites/SavedSearchableRankInfo.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/SavedSearchableRankInfo.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.favorites +package de.mm20.launcher2.searchable data class SavedSearchableRankInfo( val key: String, diff --git a/data/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt b/data/searchable/src/main/java/de/mm20/launcher2/searchable/SearchableRepository.kt similarity index 50% rename from data/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt rename to data/searchable/src/main/java/de/mm20/launcher2/searchable/SearchableRepository.kt index fd789944..b4d93121 100644 --- a/data/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/SearchableRepository.kt @@ -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? = null, excludeTypes: List? = null, manuallySorted: Boolean = false, automaticallySorted: Boolean = false, frequentlyUsed: Boolean = false, - limit: Int = 100 + hidden: Boolean = false, + limit: Int = 100, ): Flow> + fun getKeys( + includeTypes: List? = null, + excludeTypes: List? = null, + manuallySorted: Boolean = false, + automaticallySorted: Boolean = false, + frequentlyUsed: Boolean = false, + hidden: Boolean = false, + limit: Int = 100, + ): Flow> + - fun getHiddenCalendarEventKeys(): Flow> fun isPinned(searchable: SavableSearchable): Flow - fun pinItem(searchable: SavableSearchable) - fun unpinItem(searchable: SavableSearchable) fun isHidden(searchable: SavableSearchable): Flow - fun hideItem(searchable: SavableSearchable) - fun unhideItem(searchable: SavableSearchable) - fun incrementLaunchCounter(searchable: SavableSearchable) fun updateFavorites( manuallySorted: List, automaticallySorted: List, ) - fun getHiddenItems(): Flow> - fun getHiddenItemKeys(): Flow> - /** * 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): List + suspend fun getByKeys(keys: List): List 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?, excludeTypes: List?, manuallySorted: Boolean, automaticallySorted: Boolean, frequentlyUsed: Boolean, + hidden: Boolean, limit: Int ): Flow> { - 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> { - return database.searchDao().getHiddenCalendarEventKeys() + override fun getKeys( + includeTypes: List?, + excludeTypes: List?, + manuallySorted: Boolean, + automaticallySorted: Boolean, + frequentlyUsed: Boolean, + hidden: Boolean, + limit: Int + ): Flow> { + 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 { - 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 { - 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> { - return database.searchDao().getHiddenItems().map { - it.mapNotNull { fromDatabaseEntity(it).searchable } - } - } - - override fun getHiddenItemKeys(): Flow> { - 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, automaticallySorted: List ) { - 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): Flow> { - return database.searchDao().sortByRelevance(keys) + return database.searchableDao().sortByRelevance(keys) } override fun sortByWeight(keys: List): Flow> { - 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): List { - val dao = database.searchDao() - return dao.getFromKeys(keys) + override suspend fun getByKeys(keys: List): List { + val dao = database.searchableDao() + return dao.getByKeys(keys) .mapNotNull { fromDatabaseEntity(it).searchable } } diff --git a/data/favorites/src/main/java/de/mm20/launcher2/favorites/Serialization.kt b/data/searchable/src/main/java/de/mm20/launcher2/searchable/Serialization.kt similarity index 93% rename from data/favorites/src/main/java/de/mm20/launcher2/favorites/Serialization.kt rename to data/searchable/src/main/java/de/mm20/launcher2/searchable/Serialization.kt index 50bd179d..cac42fd1 100644 --- a/data/favorites/src/main/java/de/mm20/launcher2/favorites/Serialization.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/Serialization.kt @@ -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) { diff --git a/data/favorites/src/main/java/de/mm20/launcher2/favorites/TagSerialization.kt b/data/searchable/src/main/java/de/mm20/launcher2/searchable/TagSerialization.kt similarity index 95% rename from data/favorites/src/main/java/de/mm20/launcher2/favorites/TagSerialization.kt rename to data/searchable/src/main/java/de/mm20/launcher2/searchable/TagSerialization.kt index 7b92dc91..0e824148 100644 --- a/data/favorites/src/main/java/de/mm20/launcher2/favorites/TagSerialization.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/TagSerialization.kt @@ -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 diff --git a/services/backup/build.gradle.kts b/services/backup/build.gradle.kts index 1dbe3a35..b0154b02 100644 --- a/services/backup/build.gradle.kts +++ b/services/backup/build.gradle.kts @@ -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")) diff --git a/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt b/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt index 25361f8b..9dc38d51 100644 --- a/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt +++ b/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt @@ -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)) { diff --git a/services/favorites/.gitignore b/services/favorites/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/services/favorites/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/services/favorites/build.gradle.kts b/services/favorites/build.gradle.kts new file mode 100644 index 00000000..ed7b0b15 --- /dev/null +++ b/services/favorites/build.gradle.kts @@ -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")) + +} \ No newline at end of file diff --git a/services/favorites/consumer-rules.pro b/services/favorites/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/services/favorites/proguard-rules.pro b/services/favorites/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/services/favorites/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/services/favorites/src/main/AndroidManifest.xml b/services/favorites/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/services/favorites/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/services/favorites/src/main/java/de/mm20/launcher2/services/favorites/FavoritesService.kt b/services/favorites/src/main/java/de/mm20/launcher2/services/favorites/FavoritesService.kt new file mode 100644 index 00000000..23893e34 --- /dev/null +++ b/services/favorites/src/main/java/de/mm20/launcher2/services/favorites/FavoritesService.kt @@ -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? = null, + excludeTypes: List? = null, + manuallySorted: Boolean = false, + automaticallySorted: Boolean = false, + frequentlyUsed: Boolean = false, + limit: Int = 100, + ): Flow> { + return searchableRepository.get( + hidden = false, + includeTypes = includeTypes, + excludeTypes = excludeTypes, + manuallySorted = manuallySorted, + automaticallySorted = automaticallySorted, + frequentlyUsed = frequentlyUsed, + limit = limit, + ) + } + fun isPinned(searchable: SavableSearchable): Flow { + 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, + automaticallySorted: List + ) { + searchableRepository.updateFavorites( + manuallySorted = manuallySorted, + automaticallySorted = automaticallySorted + ) + } +} \ No newline at end of file diff --git a/services/favorites/src/main/java/de/mm20/launcher2/services/favorites/Module.kt b/services/favorites/src/main/java/de/mm20/launcher2/services/favorites/Module.kt new file mode 100644 index 00000000..a455c66d --- /dev/null +++ b/services/favorites/src/main/java/de/mm20/launcher2/services/favorites/Module.kt @@ -0,0 +1,7 @@ +package de.mm20.launcher2.services.favorites + +import org.koin.dsl.module + +val favoritesModule = module { + factory { FavoritesService(get()) } +} \ No newline at end of file diff --git a/services/tags/build.gradle.kts b/services/tags/build.gradle.kts index 872bb6d1..9f80594b 100644 --- a/services/tags/build.gradle.kts +++ b/services/tags/build.gradle.kts @@ -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")) } \ No newline at end of file diff --git a/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt b/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt index acbfc037..1e956709 100644 --- a/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt +++ b/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt @@ -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> { @@ -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) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 645a0e8f..317d4182 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")