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 9f8f1fc4..3592eda0 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 @@ -3,8 +3,7 @@ package de.mm20.launcher2.activity import android.app.Activity import android.os.Bundle import android.util.Log -import de.mm20.launcher2.searchable.SearchableRepository -import de.mm20.launcher2.search.data.AppShortcut +import de.mm20.launcher2.appshortcuts.AppShortcut import de.mm20.launcher2.services.favorites.FavoritesService import org.koin.android.ext.android.inject @@ -14,7 +13,7 @@ class AddItemActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val shortcut = AppShortcut.fromPinRequestIntent(this, intent) + val shortcut = AppShortcut(this, intent) if (shortcut != null) { favoritesService.pinItem(shortcut) } else { 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 e1b401a4..bf6a6ae3 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,7 +4,6 @@ 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.searchable.SearchableRepository import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.data.Tag diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/FakeSplashScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/FakeSplashScreen.kt index 8e6abe69..2e44342f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/FakeSplashScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/FakeSplashScreen.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import coil.compose.AsyncImage import de.mm20.launcher2.ktx.isAtLeastApiLevel +import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.LauncherApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlin.math.abs @@ -107,23 +107,12 @@ fun rememberSplashScreenData(searchable: SavableSearchable?): SplashScreenData { LaunchedEffect(searchable) { withContext(Dispatchers.IO) { - if (searchable is LauncherApp) { - val activityInfo = if (isAtLeastApiLevel(31)) { - searchable.launcherActivityInfo.activityInfo - } else { - try { - context.packageManager.getActivityInfo( - searchable.launcherActivityInfo.componentName, - 0 - ) - } catch (e: PackageManager.NameNotFoundException) { - null - } - } ?: return@withContext + if (searchable is Application) { + val activityInfo = searchable.getActivityInfo(context) ?: return@withContext val themeRes = activityInfo.themeResource val ctx = try { context.createPackageContext( - searchable.`package`, + searchable.componentName.packageName, Context.CONTEXT_IGNORE_SECURITY ) } catch (e: PackageManager.NameNotFoundException) { 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 7e1b864f..147e4478 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 @@ -8,7 +8,7 @@ import androidx.compose.runtime.setValue import androidx.core.app.ActivityOptionsCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import de.mm20.launcher2.searchable.SearchableRepository +import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.globalactions.GlobalActionsService import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager @@ -35,7 +35,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 searchableRepository: SearchableRepository by inject() + private val searchableRepository: SavableSearchableRepository by inject() private var isSystemInDarkMode = MutableStateFlow(false) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt index 59e27e60..65c46e75 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt @@ -1,10 +1,7 @@ package de.mm20.launcher2.ui.launcher.search import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -18,29 +15,22 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Edit -import androidx.compose.material.icons.rounded.ExpandMore import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Tag import androidx.compose.material.icons.rounded.Work import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults -import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.platform.LocalContext @@ -61,7 +51,7 @@ import de.mm20.launcher2.ui.launcher.search.common.list.ListItem import de.mm20.launcher2.ui.launcher.search.favorites.SearchFavoritesVM import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterItem import de.mm20.launcher2.ui.launcher.search.website.WebsiteItem -import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaItem +import de.mm20.launcher2.ui.launcher.search.wikipedia.ArticleItem import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.locals.LocalCardStyle @@ -99,7 +89,7 @@ fun SearchColumn( val events by viewModel.calendarResults val unitConverter by viewModel.unitConverterResults val calculator by viewModel.calculatorResults - val wikipedia by viewModel.wikipediaResults + val wikipedia by viewModel.articleResults val website by viewModel.websiteResults val hiddenResults by viewModel.hiddenResults @@ -307,7 +297,7 @@ fun SearchColumn( ) for (wiki in wikipedia) { SingleResult(highlight = bestMatch == wiki) { - WikipediaItem(wikipedia = wiki) + ArticleItem(article = wiki) } } for (ws in website) { 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 11f9c77c..c3a27107 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,23 +5,24 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import de.mm20.launcher2.searchable.SearchableRepository +import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.Ordering +import de.mm20.launcher2.search.AppProfile +import de.mm20.launcher2.search.Contact +import de.mm20.launcher2.search.File import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchService import de.mm20.launcher2.search.Searchable -import de.mm20.launcher2.search.data.AppShortcut +import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.Application +import de.mm20.launcher2.search.Article +import de.mm20.launcher2.search.CalendarEvent +import de.mm20.launcher2.search.Website import de.mm20.launcher2.search.data.Calculator -import de.mm20.launcher2.search.data.CalendarEvent -import de.mm20.launcher2.search.data.Contact -import de.mm20.launcher2.search.data.File -import de.mm20.launcher2.search.data.LauncherApp 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 @@ -43,7 +44,7 @@ import org.koin.core.component.inject class SearchVM : ViewModel(), KoinComponent { private val favoritesService: FavoritesService by inject() - private val searchableRepository: SearchableRepository by inject() + private val searchableRepository: SavableSearchableRepository by inject() private val permissionsManager: PermissionsManager by inject() private val dataStore: LauncherDataStore by inject() @@ -55,13 +56,13 @@ class SearchVM : ViewModel(), KoinComponent { val searchQuery = mutableStateOf("") val isSearchEmpty = mutableStateOf(true) - val appResults = mutableStateOf>(emptyList()) - val workAppResults = mutableStateOf>(emptyList()) + val appResults = mutableStateOf>(emptyList()) + val workAppResults = mutableStateOf>(emptyList()) val appShortcutResults = mutableStateOf>(emptyList()) val fileResults = mutableStateOf>(emptyList()) val contactResults = mutableStateOf>(emptyList()) val calendarResults = mutableStateOf>(emptyList()) - val wikipediaResults = mutableStateOf>(emptyList()) + val articleResults = mutableStateOf>(emptyList()) val websiteResults = mutableStateOf>(emptyList()) val calculatorResults = mutableStateOf>(emptyList()) val unitConverterResults = mutableStateOf>(emptyList()) @@ -185,15 +186,15 @@ class SearchVM : ViewModel(), KoinComponent { hiddenItemKeys.collectLatest { hiddenKeys -> val hidden = mutableListOf() - val apps = mutableListOf() - val workApps = mutableListOf() + val apps = mutableListOf() + val workApps = mutableListOf() val shortcuts = mutableListOf() val files = mutableListOf() val contacts = mutableListOf() val events = mutableListOf() val unitConv = mutableListOf() val calc = mutableListOf() - val wikipedia = mutableListOf() + val articles = mutableListOf
() val website = mutableListOf() val actions = mutableListOf() for (r in resultsList) { @@ -202,8 +203,8 @@ class SearchVM : ViewModel(), KoinComponent { hidden.add(r) } - r is LauncherApp && !r.isMainProfile -> workApps.add(r) - r is LauncherApp -> apps.add(r) + r is Application && r.profile == AppProfile.Work -> workApps.add(r) + r is Application -> apps.add(r) r is AppShortcut -> shortcuts.add(r) r is File -> files.add(r) r is Contact -> contacts.add(r) @@ -211,7 +212,7 @@ class SearchVM : ViewModel(), KoinComponent { r is UnitConverter -> unitConv.add(r) r is Calculator -> calc.add(r) r is Website -> website.add(r) - r is Wikipedia -> wikipedia.add(r) + r is Article -> articles.add(r) r is SearchAction -> actions.add(r) } } @@ -225,7 +226,7 @@ class SearchVM : ViewModel(), KoinComponent { calc, events, contacts, - wikipedia, + articles, website, files, actions @@ -238,7 +239,7 @@ class SearchVM : ViewModel(), KoinComponent { fileResults.value = files contactResults.value = contacts calendarResults.value = events - wikipediaResults.value = wikipedia + articleResults.value = articles websiteResults.value = website calculatorResults.value = calc unitConverterResults.value = unitConv diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt index ced47078..27dde7f0 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt @@ -66,7 +66,7 @@ import androidx.lifecycle.lifecycleScope import coil.compose.AsyncImage import com.google.accompanist.flowlayout.FlowRow import de.mm20.launcher2.crashreporter.CrashReporter -import de.mm20.launcher2.search.data.LauncherApp +import de.mm20.launcher2.search.Application import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.ShapedLauncherIcon @@ -82,12 +82,11 @@ import de.mm20.launcher2.ui.locals.LocalGridSettings import de.mm20.launcher2.ui.locals.LocalSnackbarHostState import de.mm20.launcher2.ui.modifier.scale import kotlinx.coroutines.launch -import kotlin.math.pow @Composable fun AppItem( modifier: Modifier = Modifier, - app: LauncherApp, + app: Application, onBack: () -> Unit ) { val viewModel: SearchableItemVM = listItemViewModel(key = "search-${app.key}") @@ -127,7 +126,7 @@ fun AppItem( } - app.version?.let { + app.versionName?.let { Text( text = stringResource(R.string.app_info_version, it), style = MaterialTheme.typography.bodySmall, @@ -137,7 +136,7 @@ fun AppItem( ) } Text( - text = app.`package`, + text = app.componentName.packageName, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(top = 1.dp), maxLines = 1, @@ -210,20 +209,21 @@ fun AppItem( for (shortcut in shortcuts) { val title = - shortcut.launcherShortcut.shortLabel - ?: shortcut.launcherShortcut.longLabel - ?: continue + shortcut.labelOverride ?: shortcut.label val isPinned by remember(shortcut) { viewModel.isShortcutPinned(shortcut) }.collectAsState( false ) - val icon = + val iconSizePx = InputChipDefaults.AvatarSize.toPixels() + + val icon by remember { viewModel.getShortcutIcon( context, - shortcut.launcherShortcut + shortcut, + iconSizePx.toInt() ) - } + }.collectAsState(null) InputChip( modifier = Modifier.width(IntrinsicSize.Max), @@ -233,19 +233,17 @@ fun AppItem( }, label = { Text( - title.toString(), + title, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f) ) }, avatar = { - AsyncImage( - model = icon, - contentDescription = null, - modifier = Modifier - .clip(CircleShape) - .size(InputChipDefaults.AvatarSize), + ShapedLauncherIcon( + size = InputChipDefaults.AvatarSize, + icon = { icon }, + shape = CircleShape, ) }, trailingIcon = if (LocalFavoritesEnabled.current) { @@ -311,7 +309,7 @@ fun AppItem( label = stringResource(R.string.menu_app_info), icon = Icons.Rounded.Info ) { - app.openAppInfo(context) + app.openAppDetails(context) }) toolbarActions.add( @@ -429,7 +427,7 @@ fun AppItem( @Composable fun AppItemGridPopup( - app: LauncherApp, + app: Application, show: MutableTransitionState, animationProgress: Float, origin: Rect, @@ -454,7 +452,7 @@ fun AppItemGridPopup( transformOrigin = TransformOrigin(1f, 0f) ) .offset( - x = lerp(16.dp, 0.dp, animationProgress), + x = lerp(16.dp, 0.dp, animationProgress), y = lerp(-16.dp, 0.dp, animationProgress) ), app = app, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt index dc36031f..54e23548 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarItem.kt @@ -41,13 +41,14 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntRect import androidx.lifecycle.lifecycleScope -import de.mm20.launcher2.search.data.CalendarEvent +import de.mm20.launcher2.search.CalendarEvent import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.animation.animateTextStyleAsState import de.mm20.launcher2.ui.component.DefaultToolbarAction @@ -83,12 +84,13 @@ fun CalendarItem( val snackbarHostState = LocalSnackbarHostState.current val darkMode = LocalDarkTheme.current + val secondaryColor = MaterialTheme.colorScheme.secondary Row( modifier = modifier .drawBehind { val color = TonalPalette - .fromInt(calendar.color) + .fromInt(calendar.color ?: secondaryColor.toArgb()) .tone( if (darkMode) 80 else 40 ) @@ -151,7 +153,7 @@ fun CalendarItem( style = MaterialTheme.typography.bodySmall ) } - if (calendar.description.isNotBlank()) { + if (calendar.description != null) { Row( Modifier .fillMaxWidth(), @@ -163,7 +165,7 @@ fun CalendarItem( contentDescription = null ) Text( - text = calendar.description, + text = calendar.description!!, style = MaterialTheme.typography.bodySmall ) } @@ -185,7 +187,7 @@ fun CalendarItem( ) } } - if (calendar.location.isNotBlank()) { + if (calendar.location != null) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -200,7 +202,7 @@ fun CalendarItem( contentDescription = null ) Text( - text = calendar.location, + text = calendar.location!!, style = MaterialTheme.typography.bodySmall ) } @@ -320,8 +322,7 @@ fun CalendarItemGridPopup( ) { CalendarItem( modifier = Modifier - .fillMaxWidth() - .background(Color(calendar.color).copy(alpha = 1f - animationProgress)), + .fillMaxWidth(), calendar = calendar, showDetails = true, onBack = onDismiss 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 8f887c4d..4884b661 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 @@ -4,27 +4,22 @@ import android.content.Context import android.content.pm.LauncherApps import android.content.pm.ShortcutInfo import android.graphics.drawable.Drawable -import android.service.notification.StatusBarNotification -import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.compose.ui.geometry.Rect import androidx.core.app.ActivityOptionsCompat import androidx.core.content.getSystemService -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.badges.BadgeService -import de.mm20.launcher2.files.FileRepository import de.mm20.launcher2.icons.IconService +import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.notifications.Notification import de.mm20.launcher2.notifications.NotificationRepository import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.search.File import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.AppShortcut -import de.mm20.launcher2.search.data.File -import de.mm20.launcher2.search.data.LauncherApp -import de.mm20.launcher2.search.data.LauncherShortcut +import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.Application import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.tags.TagsService import de.mm20.launcher2.ui.launcher.search.ListItemViewModel @@ -33,9 +28,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -46,7 +43,6 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { private val tagsService: TagsService by inject() private val notificationRepository: NotificationRepository by inject() private val appShortcutRepository: AppShortcutRepository by inject() - private val fileRepository: FileRepository by inject() private val permissionsManager: PermissionsManager by inject() private val searchable = MutableStateFlow(null) @@ -93,13 +89,19 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { } val notifications = searchable.flatMapLatest { searchable -> - if (searchable !is LauncherApp) emptyFlow() - else notificationRepository.notifications.map { it.filter { it.packageName == searchable.`package` && !it.isGroupSummary } } + if (searchable !is Application) emptyFlow() + else notificationRepository.notifications.map { it.filter { it.packageName == searchable.componentName.packageName && !it.isGroupSummary } } } val shortcuts = searchable.map { - if (it !is LauncherApp) emptyList() - else appShortcutRepository.getShortcutsForActivity(it.launcherActivityInfo, 5) + if (it !is Application) emptyList() + else appShortcutRepository + .findMany( + componentName = it.componentName, + user = it.user, + manifest = true, + dynamic = true, + ).first() } open fun launch(context: Context, bounds: Rect? = null): Boolean { @@ -120,7 +122,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { if (searchable.launch(context, bundle)) { favoritesService.reportLaunch(searchable) return true - } else if (searchable is LauncherApp || searchable is AppShortcut) { + } else if (searchable is Application || searchable is AppShortcut) { favoritesService.reset(searchable) } return false @@ -130,9 +132,8 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { notificationRepository.cancelNotification(notification) } - fun getShortcutIcon(context: Context, shortcut: ShortcutInfo): Drawable? { - val launcherApps = context.getSystemService() ?: return null - return launcherApps.getShortcutIconDrawable(shortcut, 0) + fun getShortcutIcon(context: Context, shortcut: AppShortcut, size: Int): Flow { + return iconService.getIcon(shortcut, size) } fun isShortcutPinned(shortcut: AppShortcut): Flow { @@ -151,10 +152,18 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent { shortcut.launch(context, null) } - fun delete() { + fun delete(context: Context) { val searchable = searchable.value ?: return - if (searchable is File) fileRepository.deleteFile(searchable) - if (searchable is LauncherShortcut) appShortcutRepository.removePinnedShortcut(searchable) + if (searchable is File) { + viewModelScope.launch { + searchable.delete(context.applicationContext) + } + } + if (searchable is AppShortcut) { + viewModelScope.launch { + searchable.delete(context.applicationContext) + } + } favoritesService.reset(searchable) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt index af5bc127..b1071733 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt @@ -1,7 +1,5 @@ package de.mm20.launcher2.ui.launcher.search.common.grid -import android.content.ComponentName -import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.MutableTransitionState @@ -42,18 +40,17 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import de.mm20.launcher2.search.Contact +import de.mm20.launcher2.search.File import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.Searchable -import de.mm20.launcher2.search.data.AppShortcut -import de.mm20.launcher2.search.data.CalendarEvent -import de.mm20.launcher2.search.data.Contact -import de.mm20.launcher2.search.data.File -import de.mm20.launcher2.search.data.LauncherApp -import de.mm20.launcher2.search.data.Website -import de.mm20.launcher2.search.data.Wikipedia +import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.Application +import de.mm20.launcher2.search.Article +import de.mm20.launcher2.search.CalendarEvent +import de.mm20.launcher2.search.Website import de.mm20.launcher2.ui.component.LauncherCard import de.mm20.launcher2.ui.component.LocalIconShape import de.mm20.launcher2.ui.component.ShapedLauncherIcon @@ -66,13 +63,12 @@ import de.mm20.launcher2.ui.launcher.search.files.FileItemGridPopup import de.mm20.launcher2.ui.launcher.search.listItemViewModel import de.mm20.launcher2.ui.launcher.search.shortcut.ShortcutItemGridPopup import de.mm20.launcher2.ui.launcher.search.website.WebsiteItemGridPopup -import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaItemGridPopup +import de.mm20.launcher2.ui.launcher.search.wikipedia.ArticleItemGridPopup import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransitionParams import de.mm20.launcher2.ui.launcher.transitions.HandleEnterHomeTransition import de.mm20.launcher2.ui.locals.LocalGridSettings import de.mm20.launcher2.ui.locals.LocalWindowSize import de.mm20.launcher2.ui.overlays.Overlay -import kotlin.math.min import kotlin.math.pow @@ -121,9 +117,9 @@ fun GridItem( val windowSize = LocalWindowSize.current - if (item is LauncherApp) { + if (item is Application) { HandleEnterHomeTransition { - val cn = ComponentName(item.`package`, item.activity) + val cn = item.componentName if ( it.componentName == cn && bounds.right > 0f && bounds.left < windowSize.width && @@ -240,7 +236,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit ) ) { when (searchable) { - is LauncherApp -> { + is Application -> { AppItemGridPopup( app = searchable, show = show, @@ -264,9 +260,9 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit ) } - is Wikipedia -> { - WikipediaItemGridPopup( - wikipedia = searchable, + is Article -> { + ArticleItemGridPopup( + article = searchable, show = show, animationProgress = animationProgress.value, origin = origin, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt index ad6faa39..d0b86656 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt @@ -9,9 +9,11 @@ import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.CalendarEvent +import de.mm20.launcher2.search.Contact +import de.mm20.launcher2.search.File import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.* import de.mm20.launcher2.ui.component.InnerCard import de.mm20.launcher2.ui.ktx.toPixels import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt index 40188f64..29e99d10 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt @@ -17,16 +17,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Message import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.Call import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Email +import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.MoreHoriz import androidx.compose.material.icons.rounded.OpenInNew import androidx.compose.material.icons.rounded.Place import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.Visibility import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material.icons.rounded.Whatsapp import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarDuration @@ -36,6 +40,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect @@ -49,7 +54,8 @@ import androidx.compose.ui.unit.roundToIntRect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import de.mm20.launcher2.ktx.tryStartActivity -import de.mm20.launcher2.search.data.Contact +import de.mm20.launcher2.search.Contact +import de.mm20.launcher2.search.ContactInfoType import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.animation.animateTextStyleAsState import de.mm20.launcher2.ui.component.Chip @@ -144,164 +150,38 @@ fun ContactItem( } AnimatedVisibility(showDetails) { + val groups = remember { + contact.contactInfos.groupBy { it.type } + } Column { - if (contact.phones.isNotEmpty()) { + for ((type, items) in groups) { Row( modifier = Modifier.padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { - Icon(Icons.Rounded.Call, contentDescription = null) + Icon(when(type) { + ContactInfoType.Phone -> Icons.Rounded.Call + ContactInfoType.Message -> Icons.AutoMirrored.Rounded.Message + ContactInfoType.Email -> Icons.Rounded.Email + ContactInfoType.Postal -> Icons.Rounded.Home + ContactInfoType.Telegram -> Icons.Rounded.Telegram + ContactInfoType.Whatsapp -> Icons.Rounded.Whatsapp + ContactInfoType.Signal -> Icons.Rounded.Signal + ContactInfoType.Other -> Icons.Rounded.MoreHoriz + }, contentDescription = null) LazyRow( modifier = Modifier .weight(1f) .padding(start = 16.dp), verticalAlignment = Alignment.CenterVertically ) { - items(contact.phones.toList()) { + items(items.toList()) { Chip( modifier = Modifier.padding(end = 16.dp), text = it.label, onClick = { - context.tryStartActivity( - Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setData(Uri.parse(it.data)) - ) - } - ) - } - } - } - } - if (contact.emails.isNotEmpty()) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Rounded.Email, contentDescription = null) - LazyRow( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - items(contact.emails.toList()) { - Chip( - modifier = Modifier.padding(end = 16.dp), - text = it.label, - onClick = { - context.tryStartActivity( - Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setData(Uri.parse(it.data)) - ) - } - ) - } - } - } - } - if (contact.signal.isNotEmpty()) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Rounded.Signal, contentDescription = null) - LazyRow( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - items(contact.signal.toList()) { - Chip( - modifier = Modifier.padding(end = 16.dp), - text = it.label, - onClick = { - context.tryStartActivity( - Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setData(Uri.parse(it.data)) - ) - } - ) - } - } - } - } - if (contact.telegram.isNotEmpty()) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Rounded.Telegram, contentDescription = null) - LazyRow( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - items(contact.telegram.toList()) { - Chip( - modifier = Modifier.padding(end = 16.dp), - text = it.label, - onClick = { - context.tryStartActivity( - Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setData(Uri.parse(it.data)) - ) - } - ) - } - } - } - } - if (contact.whatsapp.isNotEmpty()) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Rounded.WhatsApp, contentDescription = null) - LazyRow( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - items(contact.whatsapp.toList()) { - Chip( - modifier = Modifier.padding(end = 16.dp), - text = it.label, - onClick = { - context.tryStartActivity( - Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setData(Uri.parse(it.data)) - ) - } - ) - } - } - } - } - if (contact.postals.isNotEmpty()) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Rounded.Place, contentDescription = null) - LazyRow( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - items(contact.postals.toList()) { - Chip( - modifier = Modifier.padding(end = 16.dp), - text = it.label, - onClick = { - context.tryStartActivity( - Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .setData(Uri.parse(it.data)) - ) + context.tryStartActivity(it.intent) } ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt index fbd62f02..ed5717fd 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileItem.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntRect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope -import de.mm20.launcher2.search.data.File +import de.mm20.launcher2.search.File import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.animation.animateTextStyleAsState import de.mm20.launcher2.ui.component.DefaultToolbarAction @@ -236,7 +236,7 @@ fun FileItem( onDismissRequest = { showConfirmDialog = false }, confirmButton = { TextButton(onClick = { - viewModel.delete() + viewModel.delete(context) showConfirmDialog = false }) { Text(stringResource(android.R.string.ok)) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt index 92366f80..372049a3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutItem.kt @@ -53,10 +53,7 @@ import androidx.compose.ui.unit.roundToIntRect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import de.mm20.launcher2.ktx.tryStartActivity -import de.mm20.launcher2.search.data.AppShortcut -import de.mm20.launcher2.search.data.LauncherShortcut -import de.mm20.launcher2.search.data.LegacyShortcut -import de.mm20.launcher2.search.data.UnavailableShortcut +import de.mm20.launcher2.search.AppShortcut import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.animation.animateTextStyleAsState import de.mm20.launcher2.ui.component.DefaultToolbarAction @@ -101,7 +98,7 @@ fun AppShortcutItem( Column( modifier = modifier ) { - AnimatedVisibility(showDetails && shortcut is UnavailableShortcut) { + AnimatedVisibility(showDetails && shortcut.isUnavailable) { MissingPermissionBanner( modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp), text = stringResource(R.string.shortcut_unavailable_description, stringResource(R.string.app_name)), @@ -213,7 +210,7 @@ fun AppShortcutItem( action = { sheetManager.showCustomizeSearchableModal(shortcut) } )) - if (shortcut is LauncherShortcut && shortcut.launcherShortcut.isPinned) { + if (shortcut.canDelete) { toolbarActions.add(DefaultToolbarAction( label = stringResource(R.string.menu_delete), icon = Icons.Rounded.Delete, @@ -276,7 +273,7 @@ fun AppShortcutItem( text = { Text(stringResource(R.string.alert_delete_shortcut, shortcut.label)) }, confirmButton = { TextButton(onClick = { - viewModel.delete() + viewModel.delete(context) requestDelete = false }) { Text(stringResource(android.R.string.ok)) @@ -330,9 +327,3 @@ fun ShortcutItemGridPopup( } } -val AppShortcut.packageName: String? - get() = when (this) { - is LegacyShortcut -> intent.`package` - is LauncherShortcut -> launcherShortcut.`package` - else -> null - } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt index ae6ceab5..21da609d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteItem.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntRect import coil.compose.AsyncImage -import de.mm20.launcher2.search.data.Website +import de.mm20.launcher2.search.Website import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.Toolbar @@ -64,13 +64,13 @@ fun WebsiteItem( viewModel.launch(context) } ) { - if (website.image.isNotBlank()) { + if (website.imageUrl != null) { AsyncImage( modifier = Modifier .fillMaxWidth() .aspectRatio(16f / 9f) .background(MaterialTheme.colorScheme.secondaryContainer), - model = website.image, + model = website.imageUrl, contentScale = ContentScale.Crop, contentDescription = null ) @@ -93,7 +93,7 @@ fun WebsiteItem( } Text( modifier = Modifier.padding(vertical = 8.dp), - text = website.description, + text = website.description ?: "", style = MaterialTheme.typography.bodySmall ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt similarity index 91% rename from app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaItem.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt index a6a03661..4e7b1cca 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/WikipediaItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.roundToIntRect import coil.compose.AsyncImage -import de.mm20.launcher2.search.data.Wikipedia +import de.mm20.launcher2.search.Article import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.Toolbar @@ -46,18 +46,18 @@ import de.mm20.launcher2.ui.locals.LocalGridSettings import de.mm20.launcher2.ui.utils.htmlToAnnotatedString @Composable -fun WikipediaItem( +fun ArticleItem( modifier: Modifier = Modifier, - wikipedia: Wikipedia, + article: Article, onBack: (() -> Unit)? = null ) { val context = LocalContext.current - val viewModel: SearchableItemVM = listItemViewModel(key = "search-${wikipedia.key}") + val viewModel: SearchableItemVM = listItemViewModel(key = "search-${article.key}") val iconSize = LocalGridSettings.current.iconSize.dp.toPixels() - LaunchedEffect(wikipedia, iconSize) { - viewModel.init(wikipedia, iconSize.toInt()) + LaunchedEffect(article, iconSize) { + viewModel.init(article, iconSize.toInt()) } Column( @@ -65,13 +65,13 @@ fun WikipediaItem( viewModel.launch(context) } ) { - if (!wikipedia.image.isNullOrEmpty()) { + if (!article.imageUrl.isNullOrEmpty()) { AsyncImage( modifier = Modifier .fillMaxWidth() .aspectRatio(16f / 9f) .background(MaterialTheme.colorScheme.secondaryContainer), - model = wikipedia.image, + model = article.imageUrl, contentScale = ContentScale.Crop, contentDescription = null ) @@ -80,7 +80,7 @@ fun WikipediaItem( modifier = Modifier.padding(16.dp), ) { Text( - text = wikipedia.label, + text = article.label, style = MaterialTheme.typography.titleLarge ) val tags by viewModel.tags.collectAsState(emptyList()) @@ -100,7 +100,7 @@ fun WikipediaItem( ) Text( modifier = Modifier.padding(vertical = 8.dp), - text = htmlToAnnotatedString(wikipedia.text), + text = htmlToAnnotatedString(article.text), style = MaterialTheme.typography.bodySmall ) } @@ -134,7 +134,7 @@ fun WikipediaItem( label = stringResource(R.string.menu_share), icon = Icons.Rounded.Share, action = { - wikipedia.share(context) + article.share(context) } ) ) @@ -143,7 +143,7 @@ fun WikipediaItem( toolbarActions.add(DefaultToolbarAction( label = stringResource(R.string.menu_customize), icon = Icons.Rounded.Edit, - action = { sheetManager.showCustomizeSearchableModal(wikipedia) } + action = { sheetManager.showCustomizeSearchableModal(article) } )) Toolbar( @@ -160,8 +160,8 @@ fun WikipediaItem( } @Composable -fun WikipediaItemGridPopup( - wikipedia: Wikipedia, +fun ArticleItemGridPopup( + article: Article, show: MutableTransitionState, animationProgress: Float, origin: Rect, @@ -178,10 +178,10 @@ fun WikipediaItemGridPopup( shrinkTowards = Alignment.Center, ) { origin.roundToIntRect().size }, ) { - WikipediaItem( + ArticleItem( modifier = Modifier .fillMaxWidth(), - wikipedia = wikipedia, + article = article, onBack = onDismiss ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt index 48cf55bf..c661321a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt @@ -74,7 +74,7 @@ import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager -import de.mm20.launcher2.search.data.UserCalendar +import de.mm20.launcher2.calendar.UserCalendar import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.LargeMessage diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt index bcee46f0..50fa96bb 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt @@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.launcher.sheets import android.app.Activity import android.content.Context import android.content.pm.LauncherApps +import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts @@ -64,6 +65,7 @@ import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -73,6 +75,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toSize import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.search.SavableSearchable @@ -647,7 +650,6 @@ fun ShortcutPicker(viewModel: EditFavoritesSheetVM, paddingValues: PaddingValues } - val iconSize = 48.dp.toPixels().roundToInt() val activity = LocalLifecycleOwner.current as AppCompatActivity LazyColumn( contentPadding = paddingValues @@ -664,31 +666,28 @@ fun ShortcutPicker(viewModel: EditFavoritesSheetVM, paddingValues: PaddingValues } } items(shortcutActivities) { - val icon by remember(it.key) { viewModel.getIcon(it, iconSize) }.collectAsState(null) - val badge by remember(it.key) { viewModel.getBadge(it) }.collectAsState(null) + val icon by remember(it) { it.getIcon(context) }.collectAsState(null) OutlinedCard( modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp), onClick = { - val launcherApps = - context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val sender = - launcherApps.getShortcutConfigActivityIntent(it.launcherActivityInfo) - ?: return@OutlinedCard - activityLauncher.launch(IntentSenderRequest.Builder(sender).build(), null) + val intent = it.getIntent(context) ?: return@OutlinedCard run { + Log.e("MM20", "Couldn't get intent for shortcut") + } + activityLauncher.launch(IntentSenderRequest.Builder(intent).build(), null) }) { Row( modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { - ShapedLauncherIcon( - size = 48.dp, - icon = { icon }, - badge = { badge }, + AsyncImage( + model = icon, + contentDescription = null, + modifier = Modifier.size(48.dp) ) Text( - text = it.labelOverride ?: it.label, + text = it.label, modifier = Modifier.padding(start = 16.dp), style = MaterialTheme.typography.titleSmall ) 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 0a9f4182..74043535 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 @@ -19,7 +19,7 @@ import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.AppShortcut +import de.mm20.launcher2.appshortcuts.AppShortcut import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.services.favorites.FavoritesService @@ -210,7 +210,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent { fun createShortcut(context: Context, data: Intent?) { data ?: return cancelPickShortcut() - val shortcut = AppShortcut.fromPinRequestIntent(context, data) + val shortcut = AppShortcut(context, data) if (shortcut == null) { cancelPickShortcut() 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 d60c9fa7..f007a071 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 @@ -9,11 +9,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.mm20.launcher2.calendar.CalendarRepository -import de.mm20.launcher2.searchable.SearchableRepository +import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager -import de.mm20.launcher2.search.data.CalendarEvent +import de.mm20.launcher2.search.CalendarEvent import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.widgets.CalendarWidget import de.mm20.launcher2.widgets.CalendarWidgetConfig @@ -34,14 +34,14 @@ class CalendarWidgetVM : ViewModel(), KoinComponent { private val calendarRepository: CalendarRepository by inject() private val favoritesService: FavoritesService by inject() - private val searchableRepository: SearchableRepository by inject() + private val searchableRepository: SavableSearchableRepository by inject() private val widgetConfig = MutableStateFlow(CalendarWidgetConfig()) val calendarEvents = mutableStateOf>(emptyList()) val pinnedCalendarEvents = favoritesService.getFavorites( - includeTypes = listOf(CalendarEvent.Domain), + includeTypes = listOf("calendar"), automaticallySorted = true, manuallySorted = true, ).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) @@ -164,12 +164,14 @@ class CalendarWidgetVM : ViewModel(), KoinComponent { suspend fun onActive() { selectDate(LocalDate.now()) widgetConfig.collectLatest { config -> - calendarRepository.getUpcomingEvents( + calendarRepository.findMany( + from = System.currentTimeMillis(), + to = System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000L, excludeAllDayEvents = !config.allDayEvents, excludeCalendars = config.excludedCalendarIds, ).collectLatest { events -> searchableRepository.getKeys( - includeTypes = listOf(CalendarEvent.Domain), + includeTypes = listOf("calendar"), hidden = true, limit = 9999, ).collectLatest { hidden -> 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 60a75876..4ceddb73 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 @@ -11,7 +11,6 @@ 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.searchable.SearchableRepository import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout import de.mm20.launcher2.services.favorites.FavoritesService 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 09b72802..43365fc8 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 @@ -3,13 +3,13 @@ package de.mm20.launcher2.ui.settings.debug import androidx.lifecycle.ViewModel import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.icons.IconService -import de.mm20.launcher2.searchable.SearchableRepository +import de.mm20.launcher2.searchable.SavableSearchableRepository import org.koin.core.component.KoinComponent import org.koin.core.component.inject class DebugSettingsScreenVM: ViewModel(), KoinComponent { - private val searchableRepository: SearchableRepository by inject() + private val searchableRepository: SavableSearchableRepository by inject() private val customAttributesRepository: CustomAttributesRepository by inject() private val iconService: IconService by inject() suspend fun cleanUpDatabase(): Int { 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 afc32c05..fcd4c742 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 @@ -3,7 +3,7 @@ package de.mm20.launcher2.ui.settings.gestures import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import de.mm20.launcher2.searchable.SearchableRepository +import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.permissions.PermissionGroup @@ -24,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 searchableRepository: SearchableRepository by inject() + private val searchableRepository: SavableSearchableRepository by inject() private val iconService: IconService by inject() val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility) 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 e5941548..1ea120d6 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 @@ -1,20 +1,17 @@ package de.mm20.launcher2.ui.settings.hiddenitems -import android.content.ComponentName import android.content.Context -import android.content.pm.LauncherApps import android.os.Bundle -import androidx.core.content.getSystemService import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.mm20.launcher2.applications.AppRepository -import de.mm20.launcher2.searchable.SearchableRepository +import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.LauncherApp +import de.mm20.launcher2.search.Application import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -30,16 +27,16 @@ import org.koin.core.component.inject class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent { private val appRepository: AppRepository by inject() - private val searchableRepository: SearchableRepository by inject() + private val searchableRepository: SavableSearchableRepository by inject() private val iconService: IconService by inject() private val dataStore: LauncherDataStore by inject() - val allApps = appRepository.getAllInstalledApps().map { + val allApps = appRepository.findMany().map { withContext(Dispatchers.Default) { it.sorted() } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) val hiddenItems: StateFlow> = flow { val hidden = - searchableRepository.get(hidden = true).first().filter { it !is LauncherApp }.sorted() + searchableRepository.get(hidden = true).first().filter { it !is Application }.sorted() emit(hidden) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) @@ -67,15 +64,8 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent { searchable.launch(context, bundle) } - fun openAppInfo(context: Context, app: LauncherApp) { - val launcherApps = context.getSystemService()!! - - launcherApps.startAppDetailsActivity( - ComponentName(app.`package`, app.activity), - app.getUser(), - null, - null - ) + fun openAppInfo(context: Context, app: Application) { + app.openAppDetails(context) } val hiddenItemsButton = dataStore.data.map { it.searchBar.hiddenItemsButton } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt index 5aebbd00..da73b8b3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreenVM.kt @@ -12,7 +12,7 @@ import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.Settings -import de.mm20.launcher2.search.data.LauncherApp +import de.mm20.launcher2.search.Application import de.mm20.launcher2.services.favorites.FavoritesService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -224,7 +224,7 @@ class IconsSettingsScreenVM( fun getPreviewIcons(size: Int): Flow> { return columnCount.flatMapLatest { cols -> favoritesService.getFavorites( - includeTypes = listOf(LauncherApp.Domain), + includeTypes = listOf("app"), limit = cols, manuallySorted = true, automaticallySorted = true, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/media/MediaIntegrationSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/media/MediaIntegrationSettingsScreenVM.kt index a99c4d10..9afe8dba 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/media/MediaIntegrationSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/media/MediaIntegrationSettingsScreenVM.kt @@ -12,6 +12,7 @@ import de.mm20.launcher2.music.MusicService import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherDataStore +import de.mm20.launcher2.search.AppProfile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -49,8 +50,8 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent { loading.value = true viewModelScope.launch(Dispatchers.Default) { val musicApps = musicService.getInstalledPlayerPackages() - val allApps = appRepository.getAllInstalledApps().first().filter { it.isMainProfile } - .distinctBy { it.`package` } + val allApps = appRepository.findMany().first().filter { it.profile == AppProfile.Personal } + .distinctBy { it.componentName.packageName } val settings = dataStore.data.map { it.musicWidget }.first() val allowList = settings.allowListList val denyList = settings.denyListList @@ -58,10 +59,10 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent { appList.value = allApps.map { AppListItem( label = it.label, - packageName = it.`package`, - isMusicApp = musicApps.contains(it.`package`), - isChecked = allowList.contains(it.`package`) || (!denyList.contains(it.`package`) && musicApps.contains( - it.`package` + packageName = it.componentName.packageName, + isMusicApp = musicApps.contains(it.componentName.packageName), + isChecked = allowList.contains(it.componentName.packageName) || (!denyList.contains(it.componentName.packageName) && musicApps.contains( + it.componentName.packageName )), icon = iconService.getIcon(it, (32 * density).roundToInt()) .shareIn(viewModelScope, SharingStarted.WhileSubscribed(10000)) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt index ff090ab5..c9e783bc 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt @@ -55,7 +55,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent { viewModelScope.launch(Dispatchers.Default) { allTags = tagService.getAllTags().first().toSet() val items = if (tag != null) tagService.getTaggedItems(tag).first() else emptyList() - val apps = appRepository.getAllInstalledApps().first().sorted() + val apps = appRepository.findMany().first().sorted() taggedItems = items taggableApps = apps.map { app -> TaggableItem(app, items.any { app.key == it.key }) } taggableOther = items.mapNotNull { item -> diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt b/core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt similarity index 61% rename from data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt rename to core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt index dfd936fe..71e7cbb6 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/AppShortcut.kt @@ -1,17 +1,18 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.search +import android.content.ComponentName import android.content.Context -import android.content.Intent import androidx.core.content.ContextCompat -import de.mm20.launcher2.appshortcuts.R +import de.mm20.launcher2.base.R import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.TintedIconLayer -import de.mm20.launcher2.search.SavableSearchable -interface AppShortcut: SavableSearchable { +interface AppShortcut : SavableSearchable { val appName: String? + val componentName: ComponentName? + val packageName: String? override val preferDetailsOverLaunch: Boolean get() = false @@ -27,10 +28,13 @@ interface AppShortcut: SavableSearchable { ) } - companion object { - fun fromPinRequestIntent(context: Context, data: Intent): AppShortcut? { - return LauncherShortcut.fromPinRequestIntent(context, data) - ?: LegacyShortcut.fromPinRequestIntent(context, data) - } - } + val canDelete: Boolean + get() = false + + suspend fun delete(context: Context) {} + + val isUnavailable: Boolean + get() = false + + val profile: AppProfile } \ No newline at end of file diff --git a/core/base/src/main/java/de/mm20/launcher2/search/Application.kt b/core/base/src/main/java/de/mm20/launcher2/search/Application.kt new file mode 100644 index 00000000..4ad14fa6 --- /dev/null +++ b/core/base/src/main/java/de/mm20/launcher2/search/Application.kt @@ -0,0 +1,62 @@ +package de.mm20.launcher2.search + +import android.content.ComponentName +import android.content.Context +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.os.UserHandle +import androidx.core.content.ContextCompat +import de.mm20.launcher2.base.R +import de.mm20.launcher2.icons.ColorLayer +import de.mm20.launcher2.icons.StaticLauncherIcon +import de.mm20.launcher2.icons.TintedIconLayer + +enum class AppProfile { + Personal, + Work, +} + +interface Application: SavableSearchable { + override val preferDetailsOverLaunch: Boolean + get() = false + + val componentName: ComponentName + val isSystemApp: Boolean + val isSuspended: Boolean + val profile: AppProfile + val user: UserHandle + val versionName: String? + + override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { + return StaticLauncherIcon( + foregroundLayer = TintedIconLayer( + icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!, + scale = 0.65f, + color = 0xff3dda84.toInt(), + ), + backgroundLayer = ColorLayer(0xff3dda84.toInt()) + ) + } + + val canUninstall: Boolean + fun uninstall(context: Context) + fun openAppDetails(context: Context) + + val canShareApk: Boolean + suspend fun shareApkFile(context: Context) {} + + fun getStoreDetails(context: Context): StoreLink? = null + + fun getActivityInfo(context: Context): ActivityInfo? { + return try { + context.packageManager.getActivityInfo(componentName, 0) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } +} + +data class StoreLink( + val label: String, + val url: String +) \ No newline at end of file diff --git a/core/base/src/main/java/de/mm20/launcher2/search/Article.kt b/core/base/src/main/java/de/mm20/launcher2/search/Article.kt new file mode 100644 index 00000000..888d1740 --- /dev/null +++ b/core/base/src/main/java/de/mm20/launcher2/search/Article.kt @@ -0,0 +1,17 @@ +package de.mm20.launcher2.search + +import android.content.Context + +interface Article: SavableSearchable { + + val text: String + val imageUrl: String? + val sourceUrl: String + val sourceName: String + + val canShare: Boolean + fun share(context: Context) {} + + override val preferDetailsOverLaunch: Boolean + get() = false +} \ No newline at end of file diff --git a/core/base/src/main/java/de/mm20/launcher2/search/CalendarEvent.kt b/core/base/src/main/java/de/mm20/launcher2/search/CalendarEvent.kt new file mode 100644 index 00000000..a1a72439 --- /dev/null +++ b/core/base/src/main/java/de/mm20/launcher2/search/CalendarEvent.kt @@ -0,0 +1,35 @@ +package de.mm20.launcher2.search + +import android.content.Context +import de.mm20.launcher2.icons.ColorLayer +import de.mm20.launcher2.icons.StaticLauncherIcon +import de.mm20.launcher2.icons.TextLayer +import java.text.SimpleDateFormat + +interface CalendarEvent: SavableSearchable { + val color: Int? + val startTime: Long + val endTime: Long + val allDay: Boolean + val description: String? + val location: String? + val attendees: List + + + override val preferDetailsOverLaunch: Boolean + get() = true + + + override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { + val df = SimpleDateFormat("dd") + return StaticLauncherIcon( + foregroundLayer = TextLayer( + text = df.format(startTime), + color = color ?: 0, + ), + backgroundLayer = ColorLayer(color ?: 0) + ) + } + + fun openLocation(context: Context) {} +} \ No newline at end of file diff --git a/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt b/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt new file mode 100644 index 00000000..116243cb --- /dev/null +++ b/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt @@ -0,0 +1,35 @@ +package de.mm20.launcher2.search + +import android.content.Intent + +interface Contact: SavableSearchable { + val firstName: String + val lastName: String + val displayName: String + val summary: String + val contactInfos: Iterable + + override val preferDetailsOverLaunch: Boolean + get() = true +} + +/** + * Type of the contact info + * Acts as a hint for the UI, so that it can display the correct icon and group them accordingly + */ +enum class ContactInfoType { + Phone, + Message, + Email, + Postal, + Telegram, + Whatsapp, + Signal, + Other +} + +interface ContactInfo { + val type: ContactInfoType + val label: String + val intent: Intent +} \ No newline at end of file diff --git a/data/files/src/main/java/de/mm20/launcher2/search/data/File.kt b/core/base/src/main/java/de/mm20/launcher2/search/File.kt similarity index 97% rename from data/files/src/main/java/de/mm20/launcher2/search/data/File.kt rename to core/base/src/main/java/de/mm20/launcher2/search/File.kt index 8dedb75a..4349ea8e 100644 --- a/data/files/src/main/java/de/mm20/launcher2/search/data/File.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/File.kt @@ -1,13 +1,12 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.search import android.content.Context import androidx.core.content.ContextCompat -import de.mm20.launcher2.files.R +import de.mm20.launcher2.base.R import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.TintedIconLayer -import de.mm20.launcher2.search.SavableSearchable -import java.util.* +import java.util.Locale interface File : SavableSearchable { val path: String @@ -21,7 +20,7 @@ interface File : SavableSearchable { override val preferDetailsOverLaunch: Boolean get() = false - open val providerIconRes: Int? + val providerIconRes: Int? get() = null override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { diff --git a/core/base/src/main/java/de/mm20/launcher2/search/SavableSearchable.kt b/core/base/src/main/java/de/mm20/launcher2/search/SavableSearchable.kt index 8495d66c..80241e84 100644 --- a/core/base/src/main/java/de/mm20/launcher2/search/SavableSearchable.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/SavableSearchable.kt @@ -8,8 +8,6 @@ import de.mm20.launcher2.ktx.romanize import java.text.Collator interface SavableSearchable : Searchable, Comparable { - - val domain: String val key: String val label: String @@ -41,4 +39,11 @@ interface SavableSearchable : Searchable, Comparable { .compare(label1.romanize(), label2.romanize()) } + val domain: String + fun getSerializer(): SearchableSerializer + + interface Companion { + val Domain: String + } + } \ No newline at end of file diff --git a/core/base/src/main/java/de/mm20/launcher2/search/SearchableDeserializer.kt b/core/base/src/main/java/de/mm20/launcher2/search/SearchableDeserializer.kt index 0cc44198..024fe1a3 100644 --- a/core/base/src/main/java/de/mm20/launcher2/search/SearchableDeserializer.kt +++ b/core/base/src/main/java/de/mm20/launcher2/search/SearchableDeserializer.kt @@ -1,11 +1,11 @@ package de.mm20.launcher2.search interface SearchableDeserializer { - fun deserialize(serialized: String): SavableSearchable? + suspend fun deserialize(serialized: String): SavableSearchable? } class NullDeserializer: SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable? { + override suspend fun deserialize(serialized: String): SavableSearchable? { return null } diff --git a/core/base/src/main/java/de/mm20/launcher2/search/SearchableRepository.kt b/core/base/src/main/java/de/mm20/launcher2/search/SearchableRepository.kt new file mode 100644 index 00000000..20667186 --- /dev/null +++ b/core/base/src/main/java/de/mm20/launcher2/search/SearchableRepository.kt @@ -0,0 +1,8 @@ +package de.mm20.launcher2.search + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.Flow + +interface SearchableRepository { + fun search(query: String): Flow> +} \ No newline at end of file diff --git a/core/base/src/main/java/de/mm20/launcher2/search/Website.kt b/core/base/src/main/java/de/mm20/launcher2/search/Website.kt new file mode 100644 index 00000000..9ecf21f6 --- /dev/null +++ b/core/base/src/main/java/de/mm20/launcher2/search/Website.kt @@ -0,0 +1,18 @@ +package de.mm20.launcher2.search + +import android.content.Context + +interface Website: SavableSearchable { + + val url: String + val description: String? + val imageUrl: String? + val faviconUrl: String? + val color: Int? + + override val preferDetailsOverLaunch: Boolean + get() = false + + val canShare: Boolean + fun share(context: Context) {} +} \ No newline at end of file diff --git a/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt b/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt new file mode 100644 index 00000000..dd3afeef --- /dev/null +++ b/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeApp.kt @@ -0,0 +1,58 @@ +package de.mm20.launcher2.applications + +import android.content.ComponentName +import android.content.Context +import android.os.Bundle +import android.os.Process +import android.os.UserHandle +import de.mm20.launcher2.search.AppProfile +import de.mm20.launcher2.search.Application +import de.mm20.launcher2.search.NullSerializer +import de.mm20.launcher2.search.SavableSearchable +import de.mm20.launcher2.search.SearchableSerializer + +class FakeApp: Application { + override val componentName: ComponentName = ComponentName(randomString(), randomString()) + override val isSystemApp: Boolean = false + override val isSuspended: Boolean = false + override val profile: AppProfile = AppProfile.Personal + override val user: UserHandle = Process.myUserHandle() + override val versionName: String = "1.0" + override val canUninstall: Boolean = false + + override fun uninstall(context: Context) { + + } + + override fun openAppDetails(context: Context) { + + } + + override val canShareApk: Boolean = false + override val key: String = "fake://${randomString()}" + override val label: String = randomString() + + override fun overrideLabel(label: String): SavableSearchable { + return this + } + + override fun launch(context: Context, options: Bundle?): Boolean { + return false + } + + override val domain: String + get() = "fake" + + override fun getSerializer(): SearchableSerializer { + return NullSerializer() + } + + private companion object { + private fun randomString(): String { + val charset = "abcdefghijklmnopqrstuvwxyz" + return (1..10) + .map { charset.random() } + .joinToString("") + } + } +} \ No newline at end of file diff --git a/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeAppRepository.kt b/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeAppRepository.kt index b25a65f9..ecbc45c5 100644 --- a/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeAppRepository.kt +++ b/data/applications/src/debug/java/de/mm20/launcher2/applications/FakeAppRepository.kt @@ -6,8 +6,8 @@ import android.content.Intent import android.content.pm.LauncherApps import android.os.Process import androidx.core.content.getSystemService -import de.mm20.launcher2.ktx.getSerialNumber -import de.mm20.launcher2.search.data.LauncherApp +import de.mm20.launcher2.search.Application +import de.mm20.launcher2.search.SearchableRepository import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -16,51 +16,18 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map /** - * A fake implementation of [AppRepository] to simulate many installed apps. + * App repository that returns a fixed number of fake apps to simulate a large number of apps. */ -class FakeAppRepository(private val context: Context, private val fakePackages: Int) : AppRepository { +class FakeAppRepository(private val context: Context, private val fakePackages: Int) : SearchableRepository { - private val fakeApp: LauncherApp - - init { - val launcherApps = context.getSystemService()!! - fakeApp = LauncherApp( - context, - launcherApps.resolveActivity( - Intent().apply { - component = ComponentName( - context.packageName, - "de.mm20.launcher2.ui.launcher.LauncherActivity" - ) - }, - Process.myUserHandle() - ), - ) - } - - private fun randomString(): String { - val charset = "abcdefghijklmnopqrstuvwxyz" - return (1..10) - .map { charset.random() } - .joinToString("") - } - - override fun getAllInstalledApps(): Flow> { - return flowOf(buildList { - repeat(fakePackages) { - add(fakeApp.copy(`package` = randomString(), activity = randomString())) - } - }) - } - - override fun getSuspendedPackages(): Flow> { - return flowOf(emptyList()) - } - - override fun search(query: String): Flow> { + override fun search(query: String): Flow> { return if (query.isEmpty()) { - getAllInstalledApps().map { it.toImmutableList() } + buildList { + repeat(fakePackages) { + add(FakeApp()) + } + }.toImmutableList().let { flowOf(it) } } else { flowOf(persistentListOf()) } diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt index 35998590..9f806b21 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt @@ -10,28 +10,34 @@ import android.os.Handler import android.os.Looper import android.os.Process import android.os.UserHandle -import android.util.Log import de.mm20.launcher2.ktx.normalize -import de.mm20.launcher2.search.data.LauncherApp +import de.mm20.launcher2.search.Application +import de.mm20.launcher2.search.SearchableRepository import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.apache.commons.text.similarity.FuzzyScore import java.util.Locale -interface AppRepository { - fun getAllInstalledApps(): Flow> - fun getSuspendedPackages(): Flow> - fun search(query: String): Flow> +interface AppRepository : SearchableRepository { + override fun search(query: String): Flow> + + fun findMany(): Flow> } internal class AppRepositoryImpl( private val context: Context, ) : AppRepository { + private val scope = CoroutineScope(Dispatchers.Default + Job()) private val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps @@ -40,9 +46,10 @@ internal class AppRepositoryImpl( private val suspendedPackages = MutableStateFlow>(emptyList()) - private val profiles: List = - launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle()) + private val profiles: List + get() = launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle()) + private val mutex = Mutex() init { launcherApps.registerCallback(object : LauncherApps.Callback() { @@ -51,15 +58,23 @@ internal class AppRepositoryImpl( user: UserHandle, replacing: Boolean ) { - installedApps.value = - installedApps.value.filter { !packageNames.contains(it.`package`) } + scope.launch { + mutex.withLock { + installedApps.value = + installedApps.value.filter { !packageNames.contains(it.componentName.packageName) } + } + } } override fun onPackageChanged(packageName: String, user: UserHandle) { - val apps = installedApps.value.toMutableList() - apps.removeAll { packageName == it.`package` } - apps.addAll(getApplications(packageName)) - installedApps.value = apps + scope.launch { + mutex.withLock { + val apps = installedApps.value.toMutableList() + apps.removeAll { packageName == it.componentName.packageName } + apps.addAll(getApplications(packageName)) + installedApps.value = apps + } + } } override fun onPackagesAvailable( @@ -67,23 +82,35 @@ internal class AppRepositoryImpl( user: UserHandle, replacing: Boolean ) { - val apps = installedApps.value.toMutableList() - for (packageName in packageNames) { - apps.addAll(getApplications(packageName)) + scope.launch { + mutex.withLock { + val apps = installedApps.value.toMutableList() + for (packageName in packageNames) { + apps.addAll(getApplications(packageName)) + } + installedApps.value = apps + } } - installedApps.value = apps } override fun onPackageAdded(packageName: String, user: UserHandle) { - Log.d("MM20", "App installed: $packageName") - val apps = installedApps.value.toMutableList() - apps.addAll(getApplications(packageName)) - installedApps.value = apps + scope.launch { + mutex.withLock { + val apps = installedApps.value.toMutableList() + apps.addAll(getApplications(packageName)) + installedApps.value = apps + } + } } override fun onPackageRemoved(packageName: String, user: UserHandle) { - installedApps.value = - installedApps.value.filter { packageName != (it.`package`) || it.getUser() != user } + scope.launch { + mutex.withLock { + installedApps.value = + installedApps.value.filter { packageName != (it.componentName.packageName) || it.user != user } + + } + } } override fun onShortcutsChanged( @@ -91,40 +118,55 @@ internal class AppRepositoryImpl( shortcuts: MutableList, user: UserHandle ) { - super.onShortcutsChanged(packageName, shortcuts, user) onPackageChanged(packageName, user) } override fun onPackagesSuspended(packageNames: Array?, user: UserHandle?) { - super.onPackagesSuspended(packageNames, user) packageNames ?: return - suspendedPackages.value = suspendedPackages.value + packageNames + scope.launch { + mutex.withLock { + installedApps.value = installedApps.value.map { + if (packageNames.contains(it.componentName.packageName)) { + it.copy(isSuspended = true) + } else { + it + } + } + } + } } override fun onPackagesUnsuspended( packageNames: Array?, user: UserHandle? ) { - super.onPackagesUnsuspended(packageNames, user) packageNames ?: return - suspendedPackages.value = - suspendedPackages.value.filter { packageNames.contains(it) } + scope.launch { + mutex.withLock { + installedApps.value = installedApps.value.map { + if (packageNames.contains(it.componentName.packageName)) { + it.copy(isSuspended = false) + } else { + it + } + } + } + } } }, Handler(Looper.getMainLooper())) - val apps = profiles.map { p -> - try { - launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) } - } catch (e: SecurityException) { - emptyList() + scope.launch { + mutex.withLock { + val apps = profiles.map { p -> + try { + launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) } + } catch (e: SecurityException) { + emptyList() + } + }.flatten() + installedApps.value = apps } - }.flatten() - installedApps.value = apps - } - - - override fun getSuspendedPackages(): Flow> { - return suspendedPackages + } } private fun getApplications(packageName: String): List { @@ -151,6 +193,10 @@ internal class AppRepositoryImpl( return LauncherApp(context, launcherActivityInfo) } + override fun findMany(): Flow> { + return installedApps.map { it.toImmutableList() } + } + override fun search(query: String): Flow> { return installedApps.map { apps -> withContext(Dispatchers.Default) { @@ -171,10 +217,6 @@ internal class AppRepositoryImpl( } } - override fun getAllInstalledApps(): Flow> { - return installedApps - } - private fun matches(label: String, query: String): Boolean { val normalizedLabel = label.normalize() val normalizedQuery = query.normalize() diff --git a/data/applications/src/main/java/de/mm20/launcher2/search/data/AppSerialization.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/AppSerialization.kt similarity index 86% rename from data/applications/src/main/java/de/mm20/launcher2/search/data/AppSerialization.kt rename to data/applications/src/main/java/de/mm20/launcher2/applications/AppSerialization.kt index 8f5a9282..c2acb962 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/search/data/AppSerialization.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/AppSerialization.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.applications import android.content.ComponentName import android.content.Context @@ -15,8 +15,8 @@ class LauncherAppSerializer : SearchableSerializer { override fun serialize(searchable: SavableSearchable): String { searchable as LauncherApp val json = JSONObject() - json.put("package", searchable.`package`) - json.put("activity", searchable.activity) + json.put("package", searchable.componentName.packageName) + json.put("activity", searchable.componentName.className) json.put("user", searchable.userSerialNumber) return json.toString() } @@ -26,7 +26,7 @@ class LauncherAppSerializer : SearchableSerializer { } class LauncherAppDeserializer(val context: Context) : SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable? { + override suspend fun deserialize(serialized: String): SavableSearchable? { val json = JSONObject(serialized) val launcherApps = context.getSystemService()!! val userManager = context.getSystemService()!! diff --git a/data/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt similarity index 69% rename from data/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt rename to data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt index f12fa1bf..3ff83698 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/LauncherApp.kt @@ -1,9 +1,10 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.applications import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps @@ -13,43 +14,56 @@ import android.net.Uri import android.os.Bundle import android.os.Process import android.os.UserHandle -import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.getSystemService -import de.mm20.launcher2.applications.R import de.mm20.launcher2.compat.PackageManagerCompat -import de.mm20.launcher2.icons.* +import de.mm20.launcher2.icons.ColorLayer +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.icons.StaticIconLayer +import de.mm20.launcher2.icons.StaticLauncherIcon +import de.mm20.launcher2.icons.TintedIconLayer +import de.mm20.launcher2.icons.TransparentLayer import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.ktx.isAtLeastApiLevel -import de.mm20.launcher2.search.SavableSearchable +import de.mm20.launcher2.search.AppProfile +import de.mm20.launcher2.search.Application +import de.mm20.launcher2.search.SearchableSerializer +import de.mm20.launcher2.search.StoreLink import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -data class LauncherApp( - val launcherActivityInfo: LauncherActivityInfo, - override val label: String, - val `package`: String, - val activity: String, - val flags: Int, - val version: String?, +internal data class LauncherApp( + private val launcherActivityInfo: LauncherActivityInfo, + override val versionName: String?, + override val isSuspended: Boolean = false, internal val userSerialNumber: Long, override val labelOverride: String? = null, -) : SavableSearchable { +) : Application { - constructor(context: Context, launcherActivityInfo: LauncherActivityInfo): this( + override val componentName: ComponentName + get() = launcherActivityInfo.componentName + + override val label: String = launcherActivityInfo.label.toString() + + + constructor(context: Context, launcherActivityInfo: LauncherActivityInfo) : this( launcherActivityInfo, - label = launcherActivityInfo.label.toString(), - `package` = launcherActivityInfo.applicationInfo.packageName, - activity = launcherActivityInfo.name, - flags = launcherActivityInfo.applicationInfo.flags, - version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName), + versionName = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName), userSerialNumber = launcherActivityInfo.user.getSerialNumber(context) ) - val isMainProfile = launcherActivityInfo.user == Process.myUserHandle() + override val user: UserHandle + get() = launcherActivityInfo.user - val canUninstall: Boolean - get() = flags and ApplicationInfo.FLAG_SYSTEM == 0 && isMainProfile + private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle() + + override val profile: AppProfile + get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work + + override val isSystemApp: Boolean = launcherActivityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0 + + override val canUninstall: Boolean + get() = !isSystemApp && isMainProfile override val domain: String = Domain override val preferDetailsOverLaunch: Boolean = false @@ -59,22 +73,10 @@ data class LauncherApp( } override val key: String - get() = if (isMainProfile) "${domain}://$`package`:$activity" else "${domain}://$`package`:$activity:${userSerialNumber}" + // For backwards compatibility, user serial number is not included in main profile + get() = if (isMainProfile) "${domain}://${componentName.packageName}:${componentName.packageName}" + else "${domain}://${componentName.packageName}:${componentName.className}:${userSerialNumber}" - fun getUser(): UserHandle? { - return launcherActivityInfo.user - } - - override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { - return StaticLauncherIcon( - foregroundLayer = TintedIconLayer( - icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!, - scale = 0.65f, - color = 0xff3dda84.toInt(), - ), - backgroundLayer = ColorLayer(0xff3dda84.toInt()) - ) - } override suspend fun loadIcon( context: Context, @@ -132,7 +134,7 @@ data class LauncherApp( } try { launcherApps.startMainActivity( - ComponentName(`package`, activity), + componentName, launcherActivityInfo.user, null, options @@ -145,11 +147,15 @@ data class LauncherApp( return true } - fun getStoreDetails(context: Context): StoreLink? { + override fun getStoreDetails(context: Context): StoreLink? { val pm = context.packageManager return try { - val installSourceInfo = PackageManagerCompat.getInstallSource(pm, `package`) - getStoreLinkForInstaller(installSourceInfo.initiatingPackageName, `package`) + val installSourceInfo = + PackageManagerCompat.getInstallSource(pm, componentName.packageName) + getStoreLinkForInstaller( + installSourceInfo.initiatingPackageName, + componentName.packageName + ) } catch (e: PackageManager.NameNotFoundException) { null } catch (e: IllegalArgumentException) { @@ -157,37 +163,33 @@ data class LauncherApp( } } - fun uninstall(context: Context) { + override fun uninstall(context: Context) { val intent = Intent(Intent.ACTION_DELETE) - intent.data = Uri.parse("package:$`package`") + intent.data = Uri.parse("package:${componentName.packageName}") context.startActivity(intent) } - fun openAppInfo(context: Context) { + override fun openAppDetails(context: Context) { val launcherApps = context.getSystemService()!! launcherApps.startAppDetailsActivity( - ComponentName(`package`, activity), - getUser(), + componentName, + user, null, null ) } - suspend fun shareApkFile(context: Context) { + override val canShareApk: Boolean = true + override suspend fun shareApkFile(context: Context) { val launcherApps = context.getSystemService()!! val fileCopy = java.io.File( context.cacheDir, - "${`package`}-${version}.apk" + "${componentName.packageName}-${versionName}.apk" ) withContext(Dispatchers.IO) { try { - val user = getUser() - val info = if (user != null) { - launcherApps.getApplicationInfo(`package`, 0, user) - } else { - context.packageManager.getApplicationInfo(`package`, 0) - } + val info = launcherApps.getApplicationInfo(componentName.packageName, 0, user) val file = java.io.File(info.publicSourceDir) try { @@ -212,6 +214,13 @@ data class LauncherApp( } } + override fun getActivityInfo(context: Context): ActivityInfo? { + if (isAtLeastApiLevel(31)) { + return launcherActivityInfo.activityInfo + } + return super.getActivityInfo(context) + } + companion object { private fun getStoreLinkForInstaller( installerPackage: String?, @@ -225,18 +234,21 @@ data class LauncherApp( "http://www.amazon.com/gp/mas/dl/android?p=${packageName}" ) } + "com.android.vending" -> { StoreLink( "Google Play Store", "https://play.google.com/store/apps/details?id=${packageName}" ) } + "org.fdroid.fdroid", "com.aurora.adroid" -> { StoreLink( "F-Droid", "https://f-droid.org/packages/${packageName}" ) } + else -> null } } @@ -251,9 +263,8 @@ data class LauncherApp( const val Domain = "app" } -} -data class StoreLink( - val label: String, - val url: String -) \ No newline at end of file + override fun getSerializer(): SearchableSerializer { + return LauncherAppSerializer() + } +} \ No newline at end of file diff --git a/data/applications/src/main/java/de/mm20/launcher2/applications/Module.kt b/data/applications/src/main/java/de/mm20/launcher2/applications/Module.kt index 397e37f9..134ad889 100644 --- a/data/applications/src/main/java/de/mm20/launcher2/applications/Module.kt +++ b/data/applications/src/main/java/de/mm20/launcher2/applications/Module.kt @@ -1,8 +1,14 @@ package de.mm20.launcher2.applications +import de.mm20.launcher2.search.SearchableDeserializer +import de.mm20.launcher2.search.Application +import de.mm20.launcher2.search.SearchableRepository import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val applicationsModule = module { - single { AppRepositoryImpl(androidContext()) } + factory>(named()) { AppRepositoryImpl(androidContext()) } + factory { AppRepositoryImpl(androidContext()) } + factory(named(LauncherApp.Domain)) { LauncherAppDeserializer(androidContext()) } } \ No newline at end of file diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcut.kt new file mode 100644 index 00000000..b11feb8b --- /dev/null +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcut.kt @@ -0,0 +1,11 @@ +package de.mm20.launcher2.appshortcuts + +import android.content.Context +import android.content.Intent +import de.mm20.launcher2.search.AppShortcut + + +fun AppShortcut(context: Context, pinRequestIntent: Intent): AppShortcut? { + return LauncherShortcut.fromPinRequestIntent(context, pinRequestIntent) + ?: LegacyShortcut.fromPinRequestIntent(context, pinRequestIntent) +} \ No newline at end of file diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutConfigActivity.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutConfigActivity.kt new file mode 100644 index 00000000..08f378c7 --- /dev/null +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutConfigActivity.kt @@ -0,0 +1,38 @@ +package de.mm20.launcher2.appshortcuts + +import android.content.Context +import android.content.IntentSender +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.graphics.drawable.Drawable +import android.os.Process +import androidx.core.content.getSystemService +import de.mm20.launcher2.ktx.romanize +import de.mm20.launcher2.search.AppProfile +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import java.text.Collator + +class AppShortcutConfigActivity( + private val launcherActivityInfo: LauncherActivityInfo, +): Comparable { + val label = launcherActivityInfo.label.toString() + val profile: AppProfile = if (launcherActivityInfo.user == Process.myUserHandle()) AppProfile.Personal else AppProfile.Work + + fun getIcon(context: Context): Flow = flow { + val icon = launcherActivityInfo.getIcon(context.resources.displayMetrics.densityDpi) + emit(icon) + } + fun getIntent(context: Context): IntentSender? { + val launcherApps = context.getSystemService()!! + return launcherApps.getShortcutConfigActivityIntent(launcherActivityInfo) + } + + override fun compareTo(other: AppShortcutConfigActivity): Int { + + val label1 = label + val label2 = other.label + return Collator.getInstance().apply { strength = Collator.SECONDARY } + .compare(label1.romanize(), label2.romanize()) + } +} \ No newline at end of file diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt index c638e073..cc08084a 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt @@ -1,21 +1,19 @@ package de.mm20.launcher2.appshortcuts +import android.content.ComponentName import android.content.Context -import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps import android.content.pm.ShortcutInfo import android.os.Handler import android.os.Looper import android.os.Process import android.os.UserHandle -import android.util.Log import androidx.core.content.getSystemService import de.mm20.launcher2.ktx.normalize import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager -import de.mm20.launcher2.search.data.AppShortcut -import de.mm20.launcher2.search.data.LauncherApp -import de.mm20.launcher2.search.data.LauncherShortcut +import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.SearchableRepository import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -28,22 +26,25 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.withContext import org.apache.commons.text.similarity.FuzzyScore import java.util.Locale -interface AppShortcutRepository { +interface AppShortcutRepository : SearchableRepository { - fun search(query: String): Flow> - suspend fun getShortcutsForActivity( - launcherActivityInfo: LauncherActivityInfo, - count: Int = 5 - ): List + fun findMany( + componentName: ComponentName? = null, + user: UserHandle = Process.myUserHandle(), + manifest: Boolean = false, + dynamic: Boolean = false, + pinned: Boolean = false, + cached: Boolean = false, + limit: Int = 5, + ): Flow> - suspend fun getShortcutsConfigActivities(): List - - fun removePinnedShortcut(shortcut: LauncherShortcut) + suspend fun getShortcutsConfigActivities(): List } internal class AppShortcutRepositoryImpl( @@ -53,33 +54,58 @@ internal class AppShortcutRepositoryImpl( private val scope = CoroutineScope(Dispatchers.Default + Job()) - override suspend fun getShortcutsForActivity( - launcherActivityInfo: LauncherActivityInfo, - count: Int, - ) = withContext(Dispatchers.IO) { - val launcherApps = context.getSystemService()!! - if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList() - val query = LauncherApps.ShortcutQuery() - .setPackage(launcherActivityInfo.applicationInfo.packageName) - .setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST) - val shortcuts = try { - launcherApps.getShortcuts(query, launcherActivityInfo.user) - } catch (e: IllegalStateException) { - emptyList() - } - val appShortcuts = mutableListOf() - appShortcuts.addAll(shortcuts - ?.let { - if (it.size > count) it.subList(0, count) - else it - } - ?.map { - LauncherShortcut( - context, - it, + override fun findMany( + componentName: ComponentName?, + user: UserHandle, + manifest: Boolean, + dynamic: Boolean, + pinned: Boolean, + cached: Boolean, + limit: Int + ): Flow> = flow { + val shortcuts = withContext(Dispatchers.IO) { + val launcherApps = context.getSystemService()!! + if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList() + val query = LauncherApps.ShortcutQuery() + .setActivity(componentName) + .setQueryFlags( + buildQueryFlags(manifest, dynamic, pinned, cached) ) - } ?: emptyList()) - appShortcuts + val shortcuts = try { + launcherApps.getShortcuts(query, user) + } catch (e: IllegalStateException) { + emptyList() + } + val appShortcuts = mutableListOf() + appShortcuts.addAll(shortcuts + ?.let { + if (it.size > limit) it.subList(0, limit) + else it + } + ?.map { + LauncherShortcut( + context, + it, + ) + } ?: emptyList() + ) + appShortcuts + } + emit(shortcuts.toImmutableList()) + } + + private fun buildQueryFlags( + manifest: Boolean, + dynamic: Boolean, + pinned: Boolean, + cached: Boolean, + ): Int { + var flags = 0 + if (manifest) flags = flags or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST + if (dynamic) flags = flags or LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC + if (pinned) flags = flags or LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED + if (cached) flags = flags or LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED + return flags } override fun search(query: String) = channelFlow> { @@ -93,7 +119,6 @@ internal class AppShortcutRepositoryImpl( return@withContext } - shortcutChangeEmitter.collectLatest { val launcherApps = context.getSystemService() ?: return@collectLatest send( @@ -180,39 +205,16 @@ internal class AppShortcutRepositoryImpl( } }.shareIn(scope, SharingStarted.WhileSubscribed(500), 1) - override fun removePinnedShortcut(shortcut: LauncherShortcut) { - val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - if (!launcherApps.hasShortcutHostPermission()) return - val pinnedShortcutsQuery = LauncherApps.ShortcutQuery().apply { - setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) - } - val userHandle = shortcut.launcherShortcut.userHandle - val allPinned = launcherApps.getShortcuts(pinnedShortcutsQuery, userHandle) - - if (allPinned == null) { - Log.e("MM20", "Could not remove shortcut ${shortcut.key}: shortcut query returned null") - return - } - - launcherApps.pinShortcuts( - shortcut.launcherShortcut.`package`, - allPinned.filter { it.id != shortcut.launcherShortcut.id }.map { it.id }, - userHandle - ) - } - - override suspend fun getShortcutsConfigActivities(): List { + override suspend fun getShortcutsConfigActivities(): List { val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps if (!launcherApps.hasShortcutHostPermission()) return emptyList() - val results = mutableListOf() + val results = mutableListOf() val profiles = launcherApps.profiles for (profile in profiles) { val activities = launcherApps.getShortcutConfigActivityList(null, profile) results.addAll( activities.map { - LauncherApp( - context, it - ) + AppShortcutConfigActivity(it) } ) } diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt index 43d6874d..b4691d90 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt @@ -11,9 +11,6 @@ import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableSerializer -import de.mm20.launcher2.search.data.LauncherShortcut -import de.mm20.launcher2.search.data.LegacyShortcut -import de.mm20.launcher2.search.data.UnavailableShortcut import org.json.JSONObject import org.koin.core.component.KoinComponent @@ -37,7 +34,7 @@ class LauncherShortcutDeserializer( val context: Context ) : SearchableDeserializer, KoinComponent { - override fun deserialize(serialized: String): SavableSearchable? { + override suspend fun deserialize(serialized: String): SavableSearchable? { val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps val json = JSONObject(serialized) @@ -65,16 +62,9 @@ class LauncherShortcutDeserializer( } catch (e: IllegalStateException) { return null } - val pm = context.packageManager - val appName = try { - pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString() - } catch (e: PackageManager.NameNotFoundException) { - return null - } - if (shortcuts == null || shortcuts.isEmpty()) { + if (shortcuts.isNullOrEmpty()) { return null } else { - val activity = shortcuts[0].activity return LauncherShortcut( context = context, launcherShortcut = shortcuts[0], @@ -84,6 +74,21 @@ class LauncherShortcutDeserializer( } } +class UnavailableShortcutSerializer: SearchableSerializer { + override fun serialize(searchable: SavableSearchable): String? { + searchable as UnavailableShortcut + return jsonObjectOf( + "packagename" to searchable.packageName, + "id" to searchable.shortcutId, + "user" to searchable.userSerial, + ).toString() + } + + override val typePrefix: String + get() = LauncherShortcut.Domain + +} + class LegacyShortcutSerializer: SearchableSerializer { override fun serialize(searchable: SavableSearchable): String { searchable as LegacyShortcut @@ -106,7 +111,7 @@ class LegacyShortcutSerializer: SearchableSerializer { class LegacyShortcutDeserializer( val context: Context ): SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable { + override suspend fun deserialize(serialized: String): SavableSearchable { val json = JSONObject(serialized) val label = json.getString("label") val intent = Intent.parseUri(json.getString("intent"), 0) diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LauncherShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt similarity index 77% rename from data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LauncherShortcut.kt rename to data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt index 86d84384..58f579b6 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LauncherShortcut.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LauncherShortcut.kt @@ -1,6 +1,7 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.appshortcuts import android.content.ActivityNotFoundException +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.LauncherApps @@ -12,11 +13,13 @@ import android.os.Process import android.util.Log import androidx.core.content.ContextCompat import androidx.core.content.getSystemService -import de.mm20.launcher2.appshortcuts.R import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.icons.* import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.ktx.isAtLeastApiLevel +import de.mm20.launcher2.search.AppProfile +import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.SearchableSerializer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.lang.NullPointerException @@ -24,7 +27,7 @@ import java.lang.NullPointerException /** * Represents a modern (Android O+) launcher shortcut */ -data class LauncherShortcut( +internal data class LauncherShortcut( val launcherShortcut: ShortcutInfo, override val appName: String?, internal val userSerialNumber: Long, @@ -32,6 +35,11 @@ data class LauncherShortcut( ) : AppShortcut { override val domain: String = Domain + override val componentName: ComponentName? + get() = launcherShortcut.activity + + override val packageName: String + get() = launcherShortcut.`package` constructor( context: Context, @@ -48,7 +56,7 @@ data class LauncherShortcut( ) override val label: String - get() = launcherShortcut.shortLabel?.toString() ?: "" + get() = launcherShortcut.shortLabel?.toString() ?: launcherShortcut.longLabel?.toString() ?: "" override fun overrideLabel(label: String): LauncherShortcut { return this.copy(labelOverride = label) @@ -56,8 +64,10 @@ data class LauncherShortcut( override val preferDetailsOverLaunch: Boolean = false + private val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle() + override val profile: AppProfile + get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work - val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle() override val key: String get() = if (isMainProfile) { @@ -145,6 +155,31 @@ data class LauncherShortcut( ) } + override fun getSerializer(): SearchableSerializer { + return LauncherShortcutSerializer() + } + + override suspend fun delete(context: Context) { + val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + if (!launcherApps.hasShortcutHostPermission()) return + val pinnedShortcutsQuery = LauncherApps.ShortcutQuery().apply { + setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) + } + val userHandle = launcherShortcut.userHandle + val allPinned = launcherApps.getShortcuts(pinnedShortcutsQuery, userHandle) + + if (allPinned == null) { + Log.e("MM20", "Could not remove shortcut ${key}: shortcut query returned null") + return + } + + launcherApps.pinShortcuts( + launcherShortcut.`package`, + allPinned.filter { it.id != launcherShortcut.id }.map { it.id }, + userHandle + ) + } + companion object { fun fromPinRequestIntent(context: Context, data: Intent): LauncherShortcut? { val launcherApps = diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LegacyShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LegacyShortcut.kt similarity index 87% rename from data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LegacyShortcut.kt rename to data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LegacyShortcut.kt index c4eb13a0..cc9e5094 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/LegacyShortcut.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/LegacyShortcut.kt @@ -1,5 +1,6 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.appshortcuts +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.Intent.ShortcutIconResource @@ -10,8 +11,11 @@ import de.mm20.launcher2.icons.* import de.mm20.launcher2.ktx.getDrawableOrNull import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.tryStartActivity +import de.mm20.launcher2.search.AppProfile +import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.SearchableSerializer -data class LegacyShortcut( +internal data class LegacyShortcut( val intent: Intent, override val label: String, override val appName: String?, @@ -22,6 +26,9 @@ data class LegacyShortcut( override val domain = Domain override val key: String = "$domain://${intent.toUri(0)}" + override val profile: AppProfile + get() = AppProfile.Personal + override fun overrideLabel(label: String): LegacyShortcut { return this.copy(labelOverride = label) } @@ -31,7 +38,10 @@ data class LegacyShortcut( return context.tryStartActivity(intent, options) } - val packageName: String? + override val componentName: ComponentName? + get() = intent.component + + override val packageName: String? get() = intent.`package` ?: intent.component?.packageName override suspend fun loadIcon(context: Context, size: Int, themed: Boolean): LauncherIcon? { @@ -75,6 +85,10 @@ data class LegacyShortcut( ) } + override fun getSerializer(): SearchableSerializer { + return LegacyShortcutSerializer() + } + companion object { const val Domain = "legacyshortcut" diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt index 3524c53a..2a1f7b26 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt @@ -1,8 +1,15 @@ package de.mm20.launcher2.appshortcuts +import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.SearchableDeserializer +import de.mm20.launcher2.search.SearchableRepository import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val appShortcutsModule = module { - single { AppShortcutRepositoryImpl(androidContext(), get()) } + factory>(named()) { AppShortcutRepositoryImpl(androidContext(), get()) } + factory { AppShortcutRepositoryImpl(androidContext(), get()) } + factory(named(LauncherShortcut.Domain)) { LauncherShortcutDeserializer(androidContext()) } + factory(named(LegacyShortcut.Domain)) { LegacyShortcutDeserializer(androidContext()) } } \ No newline at end of file diff --git a/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/UnavailableShortcut.kt b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt similarity index 80% rename from data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/UnavailableShortcut.kt rename to data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt index c9b762aa..ce547b6b 100644 --- a/data/appshortcuts/src/main/java/de/mm20/launcher2/search/data/UnavailableShortcut.kt +++ b/data/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/UnavailableShortcut.kt @@ -1,24 +1,27 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.appshortcuts +import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import android.os.Bundle import android.os.Process import android.os.UserManager -import de.mm20.launcher2.appshortcuts.R import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.TintedIconLayer +import de.mm20.launcher2.search.AppProfile +import de.mm20.launcher2.search.AppShortcut import de.mm20.launcher2.search.SavableSearchable +import de.mm20.launcher2.search.SearchableSerializer /** * Shortcut class that is used when a [LauncherShortcut] is not available, e.g. missing permissions * when Kvaesitso is not set as default launcher. */ -class UnavailableShortcut( +internal class UnavailableShortcut( override val label: String, override val appName: String?, - val packageName: String, + override val packageName: String, val shortcutId: String, val isMainProfile: Boolean, val userSerial: Long, @@ -34,6 +37,8 @@ class UnavailableShortcut( override val labelOverride: String? get() = null + override val componentName: ComponentName? + get() = null override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { return StaticLauncherIcon( @@ -56,6 +61,14 @@ class UnavailableShortcut( return false } + override fun getSerializer(): SearchableSerializer { + return UnavailableShortcutSerializer() + } + + override val isUnavailable: Boolean = true + override val profile: AppProfile + get() = TODO("Not yet implemented") + companion object { internal operator fun invoke(context: Context, id: String, packageName: String, userSerial: Long): UnavailableShortcut? { val appInfo = try { diff --git a/data/calendar/src/main/java/de/mm20/launcher2/search/data/CalendarEvent.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/AndroidCalendarEvent.kt similarity index 61% rename from data/calendar/src/main/java/de/mm20/launcher2/search/data/CalendarEvent.kt rename to data/calendar/src/main/java/de/mm20/launcher2/calendar/AndroidCalendarEvent.kt index 4598425a..61cde7d9 100644 --- a/data/calendar/src/main/java/de/mm20/launcher2/search/data/CalendarEvent.kt +++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/AndroidCalendarEvent.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.calendar import android.content.ContentUris import android.content.Context @@ -6,50 +6,33 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.provider.CalendarContract -import de.mm20.launcher2.icons.ColorLayer -import de.mm20.launcher2.icons.StaticLauncherIcon -import de.mm20.launcher2.icons.TextLayer import de.mm20.launcher2.ktx.tryStartActivity -import de.mm20.launcher2.search.SavableSearchable +import de.mm20.launcher2.search.CalendarEvent +import de.mm20.launcher2.search.SearchableSerializer import java.net.URLEncoder -import java.text.SimpleDateFormat -data class CalendarEvent( +internal data class AndroidCalendarEvent( override val label: String, val id: Long, - val color: Int, - val startTime: Long, - val endTime: Long, - val allDay: Boolean, - val location: String, - val attendees: List, - val description: String, + override val color: Int, + override val startTime: Long, + override val endTime: Long, + override val allDay: Boolean, + override val location: String?, + override val attendees: List, + override val description: String?, val calendar: Long, override val labelOverride: String? = null, -) : SavableSearchable { +) : CalendarEvent { override val domain: String = Domain override val key: String get() = "$domain://$id" - - override val preferDetailsOverLaunch: Boolean = true - - override fun overrideLabel(label: String): CalendarEvent { + override fun overrideLabel(label: String): AndroidCalendarEvent { return this.copy(labelOverride = label) } - override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { - val df = SimpleDateFormat("dd") - return StaticLauncherIcon( - foregroundLayer = TextLayer( - text = df.format(startTime), - color = color - ), - backgroundLayer = ColorLayer(color) - ) - } - private fun getLaunchIntent(): Intent { val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id) return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -59,7 +42,8 @@ data class CalendarEvent( return context.tryStartActivity(getLaunchIntent(), options) } - fun openLocation(context: Context) { + override fun openLocation(context: Context) { + if (location == null) return context.tryStartActivity( Intent(Intent.ACTION_VIEW) .setData( @@ -76,6 +60,10 @@ data class CalendarEvent( ) } + override fun getSerializer(): SearchableSerializer { + return CalendarEventSerializer() + } + companion object { const val Domain = "calendar" } diff --git a/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt index 1bc3c095..e88a2eb1 100644 --- a/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt +++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt @@ -6,8 +6,8 @@ import android.provider.CalendarContract import androidx.core.database.getStringOrNull import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager -import de.mm20.launcher2.search.data.CalendarEvent -import de.mm20.launcher2.search.data.UserCalendar +import de.mm20.launcher2.search.CalendarEvent +import de.mm20.launcher2.search.SearchableRepository import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -18,16 +18,16 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import org.koin.core.component.KoinComponent import java.util.Calendar -interface CalendarRepository { - - fun search(query: String): Flow> - fun getUpcomingEvents( - excludeCalendars: List, - excludeAllDayEvents: Boolean - ): Flow> +interface CalendarRepository: SearchableRepository { + fun findMany( + from: Long = System.currentTimeMillis(), + to: Long = from + 14 * 24 * 60 * 60 * 1000L, + excludeCalendars: List = emptyList(), + excludeAllDayEvents: Boolean = false, + limit: Int = 999, + ): Flow> suspend fun getCalendars(): List } @@ -60,16 +60,43 @@ internal class CalendarRepositoryImpl( } + override fun findMany( + from: Long, + to: Long, + excludeCalendars: List, + excludeAllDayEvents: Boolean, + limit: Int, + ) = channelFlow> { + val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar) + hasPermission.collectLatest { + if (it) { + val events = withContext(Dispatchers.IO) { + queryCalendarEvents( + query = "", + intervalStart = from, + intervalEnd = to, + limit = limit, + excludeAllDayEvents = excludeAllDayEvents, + excludeCalendars = excludeCalendars + ) + } + send(events.toImmutableList()) + } else { + send(persistentListOf()) + } + } + } + private suspend fun queryCalendarEvents( - query: String, + query: String?, intervalStart: Long, intervalEnd: Long, limit: Int = 10, excludeAllDayEvents: Boolean = false, excludeCalendars: List = emptyList(), - ): List { + ): List { val results = withContext(Dispatchers.IO) { - val results = mutableListOf() + val results = mutableListOf() val builder = CalendarContract.Instances.CONTENT_URI.buildUpon() ContentUris.appendId(builder, intervalStart) ContentUris.appendId(builder, intervalEnd) @@ -86,10 +113,10 @@ internal class CalendarRepositoryImpl( CalendarContract.Instances.DESCRIPTION ) val selection = mutableListOf() - if (query.isNotEmpty()) selection.add("${CalendarContract.Instances.TITLE} LIKE ?") + if (query != null) selection.add("${CalendarContract.Instances.TITLE} LIKE ?") if (excludeCalendars.isNotEmpty()) selection.add("${CalendarContract.Instances.CALENDAR_ID} NOT IN (${excludeCalendars.joinToString()})") if (excludeAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0") - val selArgs = if (query.isBlank()) null else arrayOf("%$query%") + val selArgs = if (query != null) null else arrayOf("%$query%") val sort = "${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else "" val cursor = context.contentResolver.query( @@ -128,7 +155,7 @@ internal class CalendarRepositoryImpl( } else { 0 } - val event = CalendarEvent( + val event = AndroidCalendarEvent( label = cursor.getStringOrNull(1) ?: "", id = cursor.getLong(0), color = cursor.getInt(5), @@ -150,32 +177,6 @@ internal class CalendarRepositoryImpl( return results } - override fun getUpcomingEvents( - excludeCalendars: List, - excludeAllDayEvents: Boolean, - ): Flow> = channelFlow { - val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar) - hasPermission.collectLatest { - if (it) { - val now = System.currentTimeMillis() - val end = now + 14 * 24 * 60 * 60 * 1000L - val events = withContext(Dispatchers.IO) { - queryCalendarEvents( - query = "", - intervalStart = now, - intervalEnd = end, - limit = 700, - excludeAllDayEvents = excludeAllDayEvents, - excludeCalendars = excludeCalendars - ) - } - send(events) - } else { - send(emptyList()) - } - } - } - override suspend fun getCalendars(): List { if (!permissionsManager.checkPermissionOnce(PermissionGroup.Calendar)) return emptyList() return withContext(Dispatchers.IO) { diff --git a/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarSerialization.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarSerialization.kt index 7b3642ed..ab9ff03f 100644 --- a/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarSerialization.kt +++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarSerialization.kt @@ -10,13 +10,12 @@ import androidx.core.database.getStringOrNull import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableSerializer -import de.mm20.launcher2.search.data.CalendarEvent import org.json.JSONObject import java.util.* class CalendarEventSerializer: SearchableSerializer { override fun serialize(searchable: SavableSearchable): String { - searchable as CalendarEvent + searchable as AndroidCalendarEvent val json = JSONObject() json.put("id", searchable.id) return json.toString() @@ -27,7 +26,7 @@ class CalendarEventSerializer: SearchableSerializer { } class CalendarEventDeserializer(val context: Context): SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable? { + override suspend fun deserialize(serialized: String): SavableSearchable? { if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null val json = JSONObject(serialized) val id = json.getLong("id") @@ -86,7 +85,7 @@ class CalendarEventDeserializer(val context: Context): SearchableDeserializer { } else { 0 } - return CalendarEvent( + return AndroidCalendarEvent( label = title, id = id, color = color, diff --git a/data/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt index 5b5e42e7..ddc08e16 100644 --- a/data/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt +++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt @@ -1,8 +1,14 @@ package de.mm20.launcher2.calendar +import de.mm20.launcher2.search.CalendarEvent +import de.mm20.launcher2.search.SearchableDeserializer +import de.mm20.launcher2.search.SearchableRepository import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val calendarModule = module { - single { CalendarRepositoryImpl(androidContext(), get()) } + factory>(named()) { CalendarRepositoryImpl(androidContext(), get()) } + factory { CalendarRepositoryImpl(androidContext(), get()) } + factory(named(AndroidCalendarEvent.Domain)) { CalendarEventDeserializer(androidContext()) } } \ No newline at end of file diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/AndroidContact.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/AndroidContact.kt new file mode 100644 index 00000000..c0ad3281 --- /dev/null +++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/AndroidContact.kt @@ -0,0 +1,93 @@ +package de.mm20.launcher2.contacts + +import android.content.ContentUris +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.provider.ContactsContract +import androidx.core.graphics.drawable.toDrawable +import de.mm20.launcher2.icons.ColorLayer +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.icons.StaticIconLayer +import de.mm20.launcher2.icons.StaticLauncherIcon +import de.mm20.launcher2.icons.TextLayer +import de.mm20.launcher2.ktx.asBitmap +import de.mm20.launcher2.ktx.tryStartActivity +import de.mm20.launcher2.search.Contact +import de.mm20.launcher2.search.ContactInfo +import de.mm20.launcher2.search.SearchableSerializer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +internal data class AndroidContact( + internal val id: Long, + override val firstName: String, + override val lastName: String, + override val displayName: String, + override val contactInfos: Iterable, + internal val lookupKey: String, + override val labelOverride: String? = null, +) : Contact { + + + override val domain: String = Domain + override val key: String + get() = "$Domain://$id" + override val label: String + get() = displayName.takeIf { it.isNotBlank() } ?: "$firstName $lastName" + + override val summary: String + get() { + return contactInfos.distinctBy { it.label }.take(5).joinToString(separator = ", ") { it.label } + } + + override fun overrideLabel(label: String): Contact { + return copy(labelOverride = label) + } + + override fun launch(context: Context, options: Bundle?): Boolean { + val uri = + ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id) + val intent = Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return context.tryStartActivity(intent, options) + } + + override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { + val iconText = + if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else "" + + return StaticLauncherIcon( + foregroundLayer = TextLayer(text = iconText, color = 0xFF2364AA.toInt()), + backgroundLayer = ColorLayer(0xFF2364AA.toInt()) + ) + } + + override suspend fun loadIcon( + context: Context, + size: Int, + themed: Boolean, + ): LauncherIcon? { + val contentResolver = context.contentResolver + val bmp = withContext(Dispatchers.IO) { + val uri = + ContactsContract.Contacts.getLookupUri(id, lookupKey) ?: return@withContext null + ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false) + ?.asBitmap() + } ?: return null + + return StaticLauncherIcon( + foregroundLayer = StaticIconLayer( + icon = bmp.toDrawable(context.resources), + ), + backgroundLayer = ColorLayer(0xFF2364AA.toInt()) + ) + } + + override fun getSerializer(): SearchableSerializer { + return ContactSerializer() + } + + companion object { + const val Domain = "contact" + } +} \ No newline at end of file diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactInfo.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactInfo.kt new file mode 100644 index 00000000..da57873c --- /dev/null +++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactInfo.kt @@ -0,0 +1,112 @@ +package de.mm20.launcher2.contacts + +import android.content.Intent +import android.net.Uri +import android.provider.ContactsContract +import de.mm20.launcher2.search.ContactInfo +import de.mm20.launcher2.search.ContactInfoType +import java.net.URLEncoder + + +internal data class PhoneContactInfo( + val number: String, +) : ContactInfo { + override val label: String + get() = number + + override val type: ContactInfoType = ContactInfoType.Phone + + override val intent: Intent + get() = Intent(Intent.ACTION_VIEW) + .setData(Uri.parse("tel:$number")) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) +} + +internal data class MailContactInfo( + val address: String, +) : ContactInfo { + override val label: String + get() = address + + override val type: ContactInfoType = ContactInfoType.Email + + override val intent: Intent + get() = Intent(Intent.ACTION_VIEW) + .setData(Uri.parse("mailto:$address")) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) +} + +internal data class PostalContactInfo( + val address: String, +) : ContactInfo { + override val label: String + get() = address.replace("\n", ", ") + + override val type: ContactInfoType = ContactInfoType.Postal + + override val intent: Intent + get() = Intent(Intent.ACTION_VIEW) + .setData(Uri.parse("geo:0,0?q=${URLEncoder.encode(address, "utf8")}")) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) +} + +internal data class TelegramContactInfo( + override val label: String, + val userId: String, +) : ContactInfo { + + override val type: ContactInfoType = ContactInfoType.Telegram + + override val intent: Intent + get() = Intent(Intent.ACTION_VIEW) + .setData(Uri.parse("tg:openmessage?user_id=$userId")) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + internal companion object { + const val ItemType = "vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile" + } +} + +internal data class SignalContactInfo( + override val label: String, + val dataId: Long, +) : ContactInfo { + + override val type: ContactInfoType = ContactInfoType.Signal + + override val intent: Intent + get() = Intent(Intent.ACTION_VIEW) + .setData( + Uri.withAppendedPath( + ContactsContract.Data.CONTENT_URI, + dataId.toString() + ) + ) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + internal companion object { + const val ItemType = "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact" + } +} + +internal data class WhatsAppContactInfo( + override val label: String, + val dataId: Long, +) : ContactInfo { + + override val type: ContactInfoType = ContactInfoType.Whatsapp + + override val intent: Intent + get() = Intent(Intent.ACTION_VIEW) + .setData( + Uri.withAppendedPath( + ContactsContract.Data.CONTENT_URI, + dataId.toString() + ) + ) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + internal companion object { + const val ItemType = "vnd.android.cursor.item/vnd.com.whatsapp.profile" + } +} \ No newline at end of file diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt index e4f279fb..b092cb03 100644 --- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt +++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt @@ -2,9 +2,12 @@ package de.mm20.launcher2.contacts import android.content.Context import android.provider.ContactsContract +import androidx.core.database.getStringOrNull import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager -import de.mm20.launcher2.search.data.Contact +import de.mm20.launcher2.search.Contact +import de.mm20.launcher2.search.ContactInfo +import de.mm20.launcher2.search.SearchableRepository import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -12,14 +15,143 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext -interface ContactRepository { - fun search(query: String): Flow> -} - -internal class ContactRepositoryImpl( +internal class ContactRepository( private val context: Context, private val permissionsManager: PermissionsManager -) : ContactRepository { +) : SearchableRepository { + + fun get(id: Long): Flow = flow { + val rawContactsCursor = context.contentResolver.query( + ContactsContract.RawContacts.CONTENT_URI, + arrayOf(ContactsContract.RawContacts._ID), + "${ContactsContract.RawContacts.CONTACT_ID} = ?", + arrayOf(id.toString()), + null + ) + if (rawContactsCursor == null) { + emit(null) + return@flow + } + val rawContacts = mutableSetOf() + while (rawContactsCursor.moveToNext()) { + rawContacts.add(rawContactsCursor.getLong(0)) + } + rawContactsCursor.close() + if (rawContacts.isEmpty()) { + emit(null) + return@flow + } + emit(getWithRawIds(id, rawContacts)) + } + + private suspend fun getWithRawIds(id: Long, rawIds: Set): Contact? = withContext(Dispatchers.IO) { + val s = "(" + rawIds.joinToString(separator = " OR ", + transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" + + " AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"${TelegramContactInfo.ItemType}\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"${WhatsAppContactInfo.ItemType}\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"${SignalContactInfo.ItemType}\"" + + ")" + val dataCursor = context.contentResolver.query( + ContactsContract.Data.CONTENT_URI, + null, s, null, null + ) ?: return@withContext null + val contactInfos = mutableSetOf() + var firstName = "" + var lastName = "" + var displayName = "" + val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE) + val emailAddressColumn = + dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS) + val numberColumn = + dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) + val addressColumn = + dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS) + val displayNameColumn = + dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME) + val givenNameColumn = + dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) + val familyNameColumn = + dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME) + val data1Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA1) + val data3Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA3) + val idColumn = dataCursor.getColumnIndex(ContactsContract.Data._ID) + loop@ while (dataCursor.moveToNext()) { + when (dataCursor.getStringOrNull(mimeTypeColumn)) { + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> + dataCursor.getStringOrNull(emailAddressColumn)?.let { + contactInfos.add(MailContactInfo(it)) + } + + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> + dataCursor.getStringOrNull(numberColumn)?.let { + val phone = it.replace(Regex("[^+0-9]"), "") + contactInfos.add(PhoneContactInfo(phone)) + } + + ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE -> + dataCursor.getStringOrNull(addressColumn)?.let { + contactInfos.add(PostalContactInfo(it)) + } + + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { + firstName = dataCursor.getStringOrNull(givenNameColumn) ?: "" + lastName = dataCursor.getStringOrNull(familyNameColumn) ?: "" + displayName = dataCursor.getStringOrNull(displayNameColumn) ?: "" + } + + TelegramContactInfo.ItemType -> { + val data1 = dataCursor.getStringOrNull(data1Column) + ?: continue@loop + val data3 = dataCursor.getStringOrNull(data3Column) + ?: continue@loop + contactInfos.add( + TelegramContactInfo(data3.substringAfterLast(" "), data1) + ) + } + + WhatsAppContactInfo.ItemType -> { + val data1 = dataCursor.getStringOrNull(data1Column) + ?: continue@loop + val dataId = dataCursor.getLong(idColumn) + contactInfos.add(WhatsAppContactInfo("+${data1.substringBefore('@')}", dataId)) + } + + SignalContactInfo.ItemType -> { + val data1 = dataCursor.getStringOrNull(data1Column) + ?: continue@loop + val dataId = dataCursor.getLong(idColumn) + contactInfos.add(SignalContactInfo(data1, dataId)) + } + } + } + dataCursor.close() + + val lookupKeyCursor = context.contentResolver.query( + ContactsContract.Contacts.CONTENT_URI, + arrayOf(ContactsContract.Contacts.LOOKUP_KEY), + "${ContactsContract.Contacts._ID} = ?", + arrayOf(id.toString()), + null + ) ?: return@withContext null + var lookUpKey = "" + if (lookupKeyCursor.moveToNext()) { + lookUpKey = lookupKeyCursor.getString(0) + } + lookupKeyCursor.close() + + return@withContext AndroidContact( + id = id, + firstName = firstName, + lastName = lastName, + displayName = displayName, + contactInfos = contactInfos, + lookupKey = lookUpKey + ) + } override fun search(query: String): Flow> { val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts) @@ -45,7 +177,8 @@ internal class ContactRepositoryImpl( ContactsContract.RawContacts.CONTACT_ID, ContactsContract.RawContacts._ID ) - val sel = "${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ? OR ${ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE} LIKE ? OR ${ContactsContract.RawContacts.PHONETIC_NAME} LIKE ?" + val sel = + "${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ? OR ${ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE} LIKE ? OR ${ContactsContract.RawContacts.PHONETIC_NAME} LIKE ?" val selArgs = arrayOf("%$query%", "%$query%", "%$query%") val cursor = context.contentResolver.query( ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null @@ -58,7 +191,7 @@ internal class ContactRepositoryImpl( cursor.close() val results = mutableListOf() for ((id, rawIds) in contactMap) { - Contact.contactById(context, id, rawIds)?.let { results.add(it) } + getWithRawIds(id, rawIds)?.let { results.add(it) } if (results.size > 15) break } results diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactSerialization.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactSerialization.kt index 684ada52..570aed9c 100644 --- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactSerialization.kt +++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactSerialization.kt @@ -1,20 +1,17 @@ package de.mm20.launcher2.contacts -import android.Manifest -import android.content.Context -import android.content.pm.PackageManager -import android.provider.ContactsContract -import androidx.core.content.ContextCompat import de.mm20.launcher2.ktx.jsonObjectOf +import de.mm20.launcher2.permissions.PermissionGroup +import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableSerializer -import de.mm20.launcher2.search.data.Contact +import kotlinx.coroutines.flow.first import org.json.JSONObject -class ContactSerializer : SearchableSerializer { +internal class ContactSerializer : SearchableSerializer { override fun serialize(searchable: SavableSearchable): String { - searchable as Contact + searchable as AndroidContact return jsonObjectOf( "id" to searchable.id ).toString() @@ -24,28 +21,15 @@ class ContactSerializer : SearchableSerializer { get() = "contact" } -class ContactDeserializer(val context: Context) : SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable? { - if (ContextCompat.checkSelfPermission( - context, - Manifest.permission.READ_CONTACTS - ) != PackageManager.PERMISSION_GRANTED - ) return null - val id = JSONObject(serialized).getLong("id") - val rawContactsCursor = context.contentResolver.query( - ContactsContract.RawContacts.CONTENT_URI, - arrayOf(ContactsContract.RawContacts._ID), - "${ContactsContract.RawContacts.CONTACT_ID} = ?", - arrayOf(id.toString()), - null - ) ?: return null - val rawContacts = mutableSetOf() - while (rawContactsCursor.moveToNext()) { - rawContacts.add(rawContactsCursor.getLong(0)) - } - rawContactsCursor.close() - if (rawContacts.isEmpty()) return null +internal class ContactDeserializer( + private val contactRepository: ContactRepository, + private val permissionsManager: PermissionsManager +) : SearchableDeserializer { - return Contact.contactById(context, id, rawContacts) + override suspend fun deserialize(serialized: String): SavableSearchable? { + if (!permissionsManager.checkPermissionOnce(PermissionGroup.Contacts)) return null + val id = JSONObject(serialized).getLong("id") + + return contactRepository.get(id).first() } } \ No newline at end of file diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt index b19013e6..1f4dbd4b 100644 --- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt +++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt @@ -1,8 +1,14 @@ package de.mm20.launcher2.contacts +import de.mm20.launcher2.search.Contact +import de.mm20.launcher2.search.SearchableDeserializer +import de.mm20.launcher2.search.SearchableRepository import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val contactsModule = module { - single { ContactRepositoryImpl(androidContext(), get()) } + factory { ContactRepository(androidContext(), get()) } + factory>(named()) { ContactRepository(androidContext(), get()) } + factory(named(AndroidContact.Domain)) { ContactDeserializer(get(), get()) } } \ No newline at end of file diff --git a/data/contacts/src/main/java/de/mm20/launcher2/search/data/Contact.kt b/data/contacts/src/main/java/de/mm20/launcher2/search/data/Contact.kt deleted file mode 100644 index 73506f44..00000000 --- a/data/contacts/src/main/java/de/mm20/launcher2/search/data/Contact.kt +++ /dev/null @@ -1,247 +0,0 @@ -package de.mm20.launcher2.search.data - -import android.content.ContentUris -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.provider.ContactsContract -import androidx.core.database.getStringOrNull -import androidx.core.graphics.drawable.toDrawable -import de.mm20.launcher2.icons.* -import de.mm20.launcher2.ktx.asBitmap -import de.mm20.launcher2.ktx.tryStartActivity -import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.Searchable -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.net.URLEncoder - -data class Contact( - val id: Long, - val firstName: String, - val lastName: String, - val displayName: String, - val lookupKey: String, - val phones: Set, - val emails: Set, - val telegram: Set, - val whatsapp: Set, - val signal: Set, - val postals: Set, - override val labelOverride: String? = null -) : Searchable, SavableSearchable { - - override val domain: String = Domain - override val key: String - get() = "${Domain}://$id" - override val label: String - get() = "$firstName $lastName" - - override fun overrideLabel(label: String): Contact { - return this.copy(labelOverride = label) - } - - override val preferDetailsOverLaunch: Boolean = true - - val summary: String - get() { - return phones.union(emails).joinToString(separator = ", ") { it.label } - } - - override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { - val iconText = - if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else "" - - return StaticLauncherIcon( - foregroundLayer = TextLayer(text = iconText, color = 0xFF2364AA.toInt()), - backgroundLayer = ColorLayer(0xFF2364AA.toInt()) - ) - } - - override suspend fun loadIcon( - context: Context, - size: Int, - themed: Boolean, - ): LauncherIcon? { - val contentResolver = context.contentResolver - val bmp = withContext(Dispatchers.IO) { - val uri = - ContactsContract.Contacts.getLookupUri(id, lookupKey) ?: return@withContext null - ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false) - ?.asBitmap() - } ?: return null - - return StaticLauncherIcon( - foregroundLayer = StaticIconLayer( - icon = bmp.toDrawable(context.resources), - ), - backgroundLayer = ColorLayer(0xFF2364AA.toInt()) - ) - } - - override fun launch(context: Context, options: Bundle?): Boolean { - val intent = getLaunchIntent() - return context.tryStartActivity(intent, options) - } - - private fun getLaunchIntent(): Intent { - val uri = - ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id) - return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - - - companion object { - internal fun contactById(context: Context, id: Long, rawIds: Set): Contact? { - val s = "(" + rawIds.joinToString(separator = " OR ", - transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" + - " AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" + - " OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\"" + - " OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\"" + - " OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\"" + - " OR ${ContactsContract.Data.MIMETYPE} = \"vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile\"" + - " OR ${ContactsContract.Data.MIMETYPE} = \"vnd.android.cursor.item/vnd.com.whatsapp.profile\"" + - " OR ${ContactsContract.Data.MIMETYPE} = \"vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact\"" + - ")" - val dataCursor = context.contentResolver.query( - ContactsContract.Data.CONTENT_URI, - null, s, null, null - ) ?: return null - val phones = mutableSetOf() - val emails = mutableSetOf() - val telegram = mutableSetOf() - val whatsapp = mutableSetOf() - val signal = mutableSetOf() - val postals = mutableSetOf() - var firstName = "" - var lastName = "" - var displayName = "" - val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE) - val emailAddressColumn = - dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS) - val numberColumn = - dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) - val addressColumn = - dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS) - val displayNameColumn = - dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME) - val givenNameColumn = - dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) - val familyNameColumn = - dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME) - val data1Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA1) - val data3Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA3) - val idColumn = dataCursor.getColumnIndex(ContactsContract.Data._ID) - loop@ while (dataCursor.moveToNext()) { - when (dataCursor.getStringOrNull(mimeTypeColumn)) { - ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> - dataCursor.getStringOrNull(emailAddressColumn)?.let { - emails.add(ContactInfo(it, "mailto:$it")) - } - ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> - dataCursor.getStringOrNull(numberColumn)?.let { - val phone = it.replace(Regex("[^+0-9]"), "") - phones.add( - ContactInfo( - phone, - "tel:$phone" - ) - ) - } - ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE -> - dataCursor.getStringOrNull(addressColumn)?.let { - postals.add( - ContactInfo( - it.replace("\n", ", "), - "geo:0,0?q=${URLEncoder.encode(it, "utf8")}" - ) - ) - } - ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { - firstName = dataCursor.getStringOrNull(givenNameColumn) ?: "" - lastName = dataCursor.getStringOrNull(familyNameColumn) ?: "" - displayName = dataCursor.getStringOrNull(displayNameColumn) ?: "" - } - "vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile" -> { - val data1 = dataCursor.getStringOrNull(data1Column) - ?: continue@loop - val data3 = dataCursor.getStringOrNull(data3Column) - ?: continue@loop - telegram.add( - ContactInfo( - data3.substringAfterLast(" "), - "tg:openmessage?user_id=$data1" - ) - ) - } - "vnd.android.cursor.item/vnd.com.whatsapp.profile" -> { - val data1 = dataCursor.getStringOrNull(data1Column) - ?: continue@loop - val dataId = dataCursor.getLong(idColumn) - whatsapp.add( - ContactInfo( - "+${data1.substringBefore('@')}", - Uri.withAppendedPath( - ContactsContract.Data.CONTENT_URI, - dataId.toString() - ).toString() - ) - ) - } - "vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.contact" -> { - val data1 = dataCursor.getStringOrNull(data1Column) - ?: continue@loop - val dataId = dataCursor.getLong(idColumn) - signal.add( - ContactInfo( - data1, - Uri.withAppendedPath( - ContactsContract.Data.CONTENT_URI, - dataId.toString() - ).toString(), - ) - ) - } - } - } - dataCursor.close() - - val lookupKeyCursor = context.contentResolver.query( - ContactsContract.Contacts.CONTENT_URI, - arrayOf(ContactsContract.Contacts.LOOKUP_KEY), - "${ContactsContract.Contacts._ID} = ?", - arrayOf(id.toString()), - null - ) ?: return null - var lookUpKey = "" - if (lookupKeyCursor.moveToNext()) { - lookUpKey = lookupKeyCursor.getString(0) - } - lookupKeyCursor.close() - - return Contact( - id = id, - emails = emails, - phones = phones, - firstName = firstName, - lastName = lastName, - displayName = displayName, - postals = postals, - telegram = telegram, - whatsapp = whatsapp, - signal = signal, - lookupKey = lookUpKey - ) - } - - const val Domain = "contact" - - } - -} - -data class ContactInfo( - val label: String, - val data: String -) \ 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 2489228f..670344bb 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 @@ -1,10 +1,9 @@ package de.mm20.launcher2.data.customattrs -import android.database.sqlite.SQLiteDatabase import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.entities.CustomAttributeEntity -import de.mm20.launcher2.searchable.SearchableRepository +import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.search.SavableSearchable import kotlinx.collections.immutable.ImmutableList @@ -48,7 +47,7 @@ interface CustomAttributesRepository { internal class CustomAttributesRepositoryImpl( private val appDatabase: AppDatabase, - private val searchableRepository: SearchableRepository + private val searchableRepository: SavableSearchableRepository ) : CustomAttributesRepository { private val scope = CoroutineScope(Job() + Dispatchers.Default) diff --git a/data/files/build.gradle.kts b/data/files/build.gradle.kts index 0cab5a3a..d491455d 100644 --- a/data/files/build.gradle.kts +++ b/data/files/build.gradle.kts @@ -51,4 +51,5 @@ dependencies { implementation(project(":core:i18n")) implementation(project(":core:permissions")) implementation(project(":core:crashreporter")) + implementation(project(":core:preferences")) } \ No newline at end of file diff --git a/data/files/src/main/java/de/mm20/launcher2/files/FileSerialization.kt b/data/files/src/main/java/de/mm20/launcher2/files/FileSerialization.kt index 2fbfcf8b..7d0e34af 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/FileSerialization.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/FileSerialization.kt @@ -3,18 +3,22 @@ package de.mm20.launcher2.files import android.content.Context import android.provider.MediaStore import androidx.core.database.getStringOrNull +import de.mm20.launcher2.files.providers.GDriveFile +import de.mm20.launcher2.files.providers.LocalFile +import de.mm20.launcher2.files.providers.NextcloudFile +import de.mm20.launcher2.files.providers.OneDriveFile +import de.mm20.launcher2.files.providers.OwncloudFile import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableSerializer -import de.mm20.launcher2.search.data.* import org.json.JSONObject import org.koin.core.component.KoinComponent import org.koin.core.component.get -class LocalFileSerializer : SearchableSerializer { +internal class LocalFileSerializer : SearchableSerializer { override fun serialize(searchable: SavableSearchable): String { searchable as LocalFile return jsonObjectOf( @@ -26,10 +30,10 @@ class LocalFileSerializer : SearchableSerializer { get() = "file" } -class LocalFileDeserializer( +internal class LocalFileDeserializer( val context: Context ) : SearchableDeserializer, KoinComponent { - override fun deserialize(serialized: String): SavableSearchable? { + override suspend fun deserialize(serialized: String): SavableSearchable? { val permissionsManager: PermissionsManager = get() if (!permissionsManager.checkPermissionOnce( PermissionGroup.ExternalStorage @@ -73,7 +77,7 @@ class LocalFileDeserializer( } } -class GDriveFileSerializer : SearchableSerializer { +internal class GDriveFileSerializer : SearchableSerializer { override fun serialize(searchable: SavableSearchable): String { searchable as GDriveFile return jsonObjectOf( @@ -102,8 +106,8 @@ class GDriveFileSerializer : SearchableSerializer { get() = "gdrive" } -class GDriveFileDeserializer : SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable { +internal class GDriveFileDeserializer : SearchableDeserializer { + override suspend fun deserialize(serialized: String): SavableSearchable { val json = JSONObject(serialized) val id = json.getString("id") val label = json.getString("label") @@ -133,7 +137,7 @@ class GDriveFileDeserializer : SearchableDeserializer { } } -class OneDriveFileSerializer : SearchableSerializer { +internal class OneDriveFileSerializer : SearchableSerializer { override fun serialize(searchable: SavableSearchable): String { searchable as OneDriveFile return jsonObjectOf( @@ -160,8 +164,8 @@ class OneDriveFileSerializer : SearchableSerializer { get() = "onedrive" } -class OneDriveFileDeserializer : SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable { +internal class OneDriveFileDeserializer : SearchableDeserializer { + override suspend fun deserialize(serialized: String): SavableSearchable { val json = JSONObject(serialized) val fileId = json.getString("id") val label = json.getString("label") @@ -188,7 +192,7 @@ class OneDriveFileDeserializer : SearchableDeserializer { } } -class NextcloudFileSerializer : SearchableSerializer { +internal class NextcloudFileSerializer : SearchableSerializer { override fun serialize(searchable: SavableSearchable): String { searchable as NextcloudFile return jsonObjectOf( @@ -215,8 +219,8 @@ class NextcloudFileSerializer : SearchableSerializer { get() = "nextcloud" } -class NextcloudFileDeserializer : SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable { +internal class NextcloudFileDeserializer : SearchableDeserializer { + override suspend fun deserialize(serialized: String): SavableSearchable { val json = JSONObject(serialized) val id = json.getLong("id") val label = json.getString("label") @@ -241,7 +245,7 @@ class NextcloudFileDeserializer : SearchableDeserializer { } } -class OwncloudFileSerializer : SearchableSerializer { +internal class OwncloudFileSerializer : SearchableSerializer { override fun serialize(searchable: SavableSearchable): String { searchable as OwncloudFile return jsonObjectOf( @@ -268,8 +272,8 @@ class OwncloudFileSerializer : SearchableSerializer { get() = "owncloud" } -class OwncloudFileDeserializer : SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable { +internal class OwncloudFileDeserializer : SearchableDeserializer { + override suspend fun deserialize(serialized: String): SavableSearchable { val json = JSONObject(serialized) val id = json.getLong("id") val label = json.getString("label") diff --git a/data/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt b/data/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt index 74792735..f02bdd9f 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt @@ -9,34 +9,23 @@ import de.mm20.launcher2.files.providers.OwncloudFileProvider import de.mm20.launcher2.nextcloud.NextcloudApiHelper import de.mm20.launcher2.owncloud.OwncloudClient import de.mm20.launcher2.permissions.PermissionsManager -import de.mm20.launcher2.search.data.File -import kotlinx.collections.immutable.ImmutableList +import de.mm20.launcher2.preferences.LauncherDataStore +import de.mm20.launcher2.search.File +import de.mm20.launcher2.search.SearchableRepository import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map -interface FileRepository { - fun search( - query: String, - local: Boolean = true, - gdrive: Boolean = true, - onedrive: Boolean = true, - nextcloud: Boolean = true, - owncloud: Boolean = true, - ): Flow> - - fun deleteFile(file: File) -} - -internal class FileRepositoryImpl( +internal class FileRepository( private val context: Context, private val permissionsManager: PermissionsManager, -) : FileRepository { + private val dataStore: LauncherDataStore, +) : SearchableRepository { private val scope = CoroutineScope(Job() + Dispatchers.Default) @@ -49,39 +38,28 @@ internal class FileRepositoryImpl( override fun search( query: String, - local: Boolean, - gdrive: Boolean, - onedrive: Boolean, - nextcloud: Boolean, - owncloud: Boolean ) = channelFlow { if (query.isBlank()) { send(persistentListOf()) return@channelFlow } - val providers = mutableListOf() + dataStore.data.map { it.fileSearch }.collectLatest { + val providers = mutableListOf() - if (local) providers.add(LocalFileProvider(context, permissionsManager)) - if (gdrive) providers.add(GDriveFileProvider(context)) - if (nextcloud) providers.add(NextcloudFileProvider(nextcloudClient)) - if (owncloud) providers.add(OwncloudFileProvider(owncloudClient)) + if (it.localFiles) providers.add(LocalFileProvider(context, permissionsManager)) + if (it.gdrive) providers.add(GDriveFileProvider(context)) + if (it.nextcloud) providers.add(NextcloudFileProvider(nextcloudClient)) + if (it.owncloud) providers.add(OwncloudFileProvider(owncloudClient)) - if (providers.isEmpty()) { - send(persistentListOf()) - return@channelFlow - } - val results = mutableListOf() - for (provider in providers) { - results.addAll(provider.search(query)) - send(results.toImmutableList()) - } - } - - override fun deleteFile(file: File) { - scope.launch { - if (file.isDeletable) { - file.delete(context) + if (providers.isEmpty()) { + send(persistentListOf()) + return@collectLatest + } + val results = mutableListOf() + for (provider in providers) { + results.addAll(provider.search(query)) + send(results.toImmutableList()) } } } diff --git a/data/files/src/main/java/de/mm20/launcher2/files/Module.kt b/data/files/src/main/java/de/mm20/launcher2/files/Module.kt index cb93ecde..05df5c1c 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/Module.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/Module.kt @@ -1,8 +1,22 @@ package de.mm20.launcher2.files +import de.mm20.launcher2.files.providers.GDriveFile +import de.mm20.launcher2.files.providers.LocalFile +import de.mm20.launcher2.files.providers.NextcloudFile +import de.mm20.launcher2.files.providers.OneDriveFile +import de.mm20.launcher2.files.providers.OwncloudFile +import de.mm20.launcher2.search.File +import de.mm20.launcher2.search.SearchableDeserializer +import de.mm20.launcher2.search.SearchableRepository import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val filesModule = module { - single { FileRepositoryImpl(androidContext(), get()) } + factory>(named()) { FileRepository(androidContext(), get(), get()) } + factory(named(LocalFile.Domain)) { LocalFileDeserializer(androidContext()) } + factory(named(OwncloudFile.Domain)) { OwncloudFileDeserializer() } + factory(named(NextcloudFile.Domain)) { NextcloudFileDeserializer() } + factory(named(OneDriveFile.Domain)) { OneDriveFileDeserializer() } + factory(named(GDriveFile.Domain)) { GDriveFileDeserializer() } } \ No newline at end of file diff --git a/data/files/src/main/java/de/mm20/launcher2/files/providers/FileProvider.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/FileProvider.kt index d30b9f8a..2ab5374c 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/providers/FileProvider.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/FileProvider.kt @@ -1,7 +1,7 @@ package de.mm20.launcher2.files.providers -import de.mm20.launcher2.search.data.File +import de.mm20.launcher2.search.File -interface FileProvider { +internal interface FileProvider { suspend fun search(query: String): List } \ No newline at end of file diff --git a/data/files/src/main/java/de/mm20/launcher2/search/data/GDriveFile.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFile.kt similarity index 80% rename from data/files/src/main/java/de/mm20/launcher2/search/data/GDriveFile.kt rename to data/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFile.kt index 08157875..6e80c10b 100644 --- a/data/files/src/main/java/de/mm20/launcher2/search/data/GDriveFile.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFile.kt @@ -1,13 +1,16 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.files.providers import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import de.mm20.launcher2.files.GDriveFileSerializer import de.mm20.launcher2.files.R import de.mm20.launcher2.ktx.tryStartActivity +import de.mm20.launcher2.search.File +import de.mm20.launcher2.search.SearchableSerializer -data class GDriveFile( +internal data class GDriveFile( val fileId: String, override val label: String, override val path: String, @@ -43,6 +46,10 @@ data class GDriveFile( return context.tryStartActivity(getLaunchIntent(), options) } + override fun getSerializer(): SearchableSerializer { + return GDriveFileSerializer() + } + companion object { const val Domain = "gdrive" } diff --git a/data/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt index f6287616..1b3b1ff6 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/GDriveFileProvider.kt @@ -4,8 +4,7 @@ import android.content.Context import de.mm20.launcher2.files.R import de.mm20.launcher2.gservices.DriveFileMeta import de.mm20.launcher2.gservices.GoogleApiHelper -import de.mm20.launcher2.search.data.File -import de.mm20.launcher2.search.data.GDriveFile +import de.mm20.launcher2.search.File internal class GDriveFileProvider( private val context: Context diff --git a/data/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/LocalFile.kt similarity index 97% rename from data/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt rename to data/files/src/main/java/de/mm20/launcher2/files/providers/LocalFile.kt index 242995da..99f84cb8 100644 --- a/data/files/src/main/java/de/mm20/launcher2/search/data/LocalFile.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/LocalFile.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.files.providers import android.content.Context import android.content.Intent @@ -16,17 +16,21 @@ import android.util.Size import androidx.core.content.FileProvider import androidx.exifinterface.media.ExifInterface import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.files.LocalFileSerializer import de.mm20.launcher2.files.R import de.mm20.launcher2.icons.* import de.mm20.launcher2.ktx.formatToString import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.media.ThumbnailUtilsCompat +import de.mm20.launcher2.search.File +import de.mm20.launcher2.search.SearchableSerializer import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.withContext import java.io.IOException import java.io.File as JavaIOFile -data class LocalFile( +internal data class LocalFile( val id: Long, override val path: String, override val mimeType: String, @@ -186,7 +190,7 @@ data class LocalFile( val file = java.io.File(path) - withContext(Dispatchers.IO) { + withContext(NonCancellable + Dispatchers.IO) { file.deleteRecursively() context.contentResolver.delete( @@ -352,4 +356,8 @@ data class LocalFile( shareIntent.type = mimeType context.startActivity(Intent.createChooser(shareIntent, null)) } + + override fun getSerializer(): SearchableSerializer { + return LocalFileSerializer() + } } \ No newline at end of file diff --git a/data/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt index a8a7d022..74e2a591 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/LocalFileProvider.kt @@ -1,13 +1,11 @@ package de.mm20.launcher2.files.providers import android.content.Context -import android.provider.DocumentsContract import android.provider.MediaStore import androidx.core.database.getStringOrNull import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager -import de.mm20.launcher2.search.data.File -import de.mm20.launcher2.search.data.LocalFile +import de.mm20.launcher2.search.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/data/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFile.kt similarity index 85% rename from data/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt rename to data/files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFile.kt index cb2a7d90..fb7e60f3 100644 --- a/data/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFile.kt @@ -1,14 +1,17 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.files.providers import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle +import de.mm20.launcher2.files.NextcloudFileSerializer import de.mm20.launcher2.files.R import de.mm20.launcher2.ktx.tryStartActivity +import de.mm20.launcher2.search.File +import de.mm20.launcher2.search.SearchableSerializer -data class NextcloudFile( +internal data class NextcloudFile( val fileId: Long, override val label: String, override val path: String, @@ -45,6 +48,10 @@ data class NextcloudFile( return context.tryStartActivity(getLaunchIntent(context), options) } + override fun getSerializer(): SearchableSerializer { + return NextcloudFileSerializer() + } + companion object { const val Domain = "nextcloud" diff --git a/data/files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFileProvider.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFileProvider.kt index e7f08399..f166703c 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFileProvider.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/NextcloudFileProvider.kt @@ -2,8 +2,7 @@ package de.mm20.launcher2.files.providers import de.mm20.launcher2.files.R import de.mm20.launcher2.nextcloud.NextcloudApiHelper -import de.mm20.launcher2.search.data.File -import de.mm20.launcher2.search.data.NextcloudFile +import de.mm20.launcher2.search.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlin.math.min diff --git a/data/files/src/main/java/de/mm20/launcher2/search/data/OneDriveFile.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFile.kt similarity index 79% rename from data/files/src/main/java/de/mm20/launcher2/search/data/OneDriveFile.kt rename to data/files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFile.kt index 42b40dbb..5862a329 100644 --- a/data/files/src/main/java/de/mm20/launcher2/search/data/OneDriveFile.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/OneDriveFile.kt @@ -1,13 +1,16 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.files.providers import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import de.mm20.launcher2.files.OneDriveFileSerializer import de.mm20.launcher2.files.R import de.mm20.launcher2.ktx.tryStartActivity +import de.mm20.launcher2.search.File +import de.mm20.launcher2.search.SearchableSerializer -data class OneDriveFile( +internal data class OneDriveFile( val fileId: String, override val label: String, override val path: String, @@ -42,6 +45,10 @@ data class OneDriveFile( return context.tryStartActivity(getLaunchIntent(), options) } + override fun getSerializer(): SearchableSerializer { + return OneDriveFileSerializer() + } + companion object { const val Domain = "onedrive" } diff --git a/data/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFile.kt similarity index 80% rename from data/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt rename to data/files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFile.kt index 75bf48db..4f77ffa1 100644 --- a/data/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFile.kt @@ -1,13 +1,16 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.files.providers import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import de.mm20.launcher2.files.OwncloudFileSerializer import de.mm20.launcher2.files.R import de.mm20.launcher2.ktx.tryStartActivity +import de.mm20.launcher2.search.File +import de.mm20.launcher2.search.SearchableSerializer -data class OwncloudFile( +internal data class OwncloudFile( val fileId: Long, override val label: String, override val path: String, @@ -42,6 +45,10 @@ data class OwncloudFile( return context.tryStartActivity(getLaunchIntent(), options) } + override fun getSerializer(): SearchableSerializer { + return OwncloudFileSerializer() + } + companion object { const val Domain = "owncloud" } diff --git a/data/files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFileProvider.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFileProvider.kt index f7c6ce7c..13696870 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFileProvider.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/OwncloudFileProvider.kt @@ -2,8 +2,7 @@ package de.mm20.launcher2.files.providers import de.mm20.launcher2.files.R import de.mm20.launcher2.owncloud.OwncloudClient -import de.mm20.launcher2.search.data.File -import de.mm20.launcher2.search.data.OwncloudFile +import de.mm20.launcher2.search.File internal class OwncloudFileProvider( private val owncloudClient: OwncloudClient diff --git a/data/searchable/build.gradle.kts b/data/searchable/build.gradle.kts index ed12a6a2..f7cd9ed8 100644 --- a/data/searchable/build.gradle.kts +++ b/data/searchable/build.gradle.kts @@ -45,12 +45,7 @@ dependencies { implementation(project(":data:calendar")) implementation(project(":core:database")) implementation(project(":core:preferences")) - implementation(project(":data:applications")) - implementation(project(":data:appshortcuts")) - implementation(project(":data:contacts")) implementation(project(":core:ktx")) - implementation(project(":data:files")) - implementation(project(":data:websites")) implementation(project(":data:wikipedia")) implementation(project(":services:badges")) implementation(project(":core:crashreporter")) diff --git a/data/searchable/src/main/java/de/mm20/launcher2/search/data/Tag.kt b/data/searchable/src/main/java/de/mm20/launcher2/search/data/Tag.kt index 8e3c363a..ae1f5e95 100644 --- a/data/searchable/src/main/java/de/mm20/launcher2/search/data/Tag.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/search/data/Tag.kt @@ -6,6 +6,8 @@ import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.TextLayer import de.mm20.launcher2.search.SavableSearchable +import de.mm20.launcher2.search.SearchableSerializer +import de.mm20.launcher2.searchable.TagSerializer data class Tag( val tag: String, @@ -33,6 +35,10 @@ data class Tag( ) } + override fun getSerializer(): SearchableSerializer { + return TagSerializer() + } + companion object { const val Domain = "tag" } 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 index 2ecfc429..eaa2f109 100644 --- a/data/searchable/src/main/java/de/mm20/launcher2/searchable/Module.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/Module.kt @@ -1,8 +1,12 @@ package de.mm20.launcher2.searchable +import de.mm20.launcher2.search.SearchableDeserializer +import de.mm20.launcher2.search.data.Tag import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val searchableModule = module { - single { SearchableRepositoryImpl(androidContext(), get(), get()) } + factory { SavableSearchableRepositoryImpl(androidContext(), get(), get()) } + factory(named(Tag.Domain)) { TagDeserializer() } } \ No newline at end of file diff --git a/data/searchable/src/main/java/de/mm20/launcher2/searchable/SearchableRepository.kt b/data/searchable/src/main/java/de/mm20/launcher2/searchable/SavableSearchableRepository.kt similarity index 95% rename from data/searchable/src/main/java/de/mm20/launcher2/searchable/SearchableRepository.kt rename to data/searchable/src/main/java/de/mm20/launcher2/searchable/SavableSearchableRepository.kt index a9e9856d..4d4ea69d 100644 --- a/data/searchable/src/main/java/de/mm20/launcher2/searchable/SearchableRepository.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/SavableSearchableRepository.kt @@ -24,9 +24,13 @@ import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONException import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.error.InstanceCreationException +import org.koin.core.error.NoBeanDefFoundException +import org.koin.core.qualifier.named import java.io.File -interface SearchableRepository { +interface SavableSearchableRepository { fun insert( searchable: SavableSearchable, @@ -114,11 +118,11 @@ interface SearchableRepository { suspend fun cleanupDatabase(): Int } -internal class SearchableRepositoryImpl( +internal class SavableSearchableRepositoryImpl( private val context: Context, private val database: AppDatabase, private val dataStore: LauncherDataStore -) : SearchableRepository, KoinComponent { +) : SavableSearchableRepository, KoinComponent { private val scope = CoroutineScope(Job() + Dispatchers.Default) @@ -348,10 +352,17 @@ internal class SearchableRepositoryImpl( return database.searchableDao().sortByWeight(keys) } - private fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable { - val deserializer: SearchableDeserializer = - getDeserializer(context, entity.type) - val searchable = deserializer.deserialize(entity.serializedSearchable) + private suspend fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable { + val deserializer: SearchableDeserializer? = try { + get(named(entity.type)) + } catch (e: NoBeanDefFoundException) { + CrashReporter.logException(e) + null + } catch (e: InstanceCreationException) { + CrashReporter.logException(e) + null + } + val searchable = deserializer?.deserialize(entity.serializedSearchable) if (searchable == null) removeInvalidItem(entity.key) return SavedSearchable( key = entity.key, diff --git a/data/searchable/src/main/java/de/mm20/launcher2/searchable/SavedSearchable.kt b/data/searchable/src/main/java/de/mm20/launcher2/searchable/SavedSearchable.kt index 41551a56..27ad0069 100644 --- a/data/searchable/src/main/java/de/mm20/launcher2/searchable/SavedSearchable.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/SavedSearchable.kt @@ -15,9 +15,7 @@ data class SavedSearchable( var weight: Double ) { fun toDatabaseEntity(): SavedSearchableEntity? { - val serializer = getSerializer(searchable) - - val data = searchable?.let { serializer.serialize(it) } ?: return null + val data = searchable?.serialize() ?: return null return SavedSearchableEntity( key = key, diff --git a/data/searchable/src/main/java/de/mm20/launcher2/searchable/Serialization.kt b/data/searchable/src/main/java/de/mm20/launcher2/searchable/Serialization.kt index cac42fd1..4fbf96f3 100644 --- a/data/searchable/src/main/java/de/mm20/launcher2/searchable/Serialization.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/Serialization.kt @@ -1,114 +1,8 @@ package de.mm20.launcher2.searchable -import android.content.Context -import de.mm20.launcher2.appshortcuts.LauncherShortcutDeserializer -import de.mm20.launcher2.appshortcuts.LauncherShortcutSerializer -import de.mm20.launcher2.appshortcuts.LegacyShortcutDeserializer -import de.mm20.launcher2.appshortcuts.LegacyShortcutSerializer -import de.mm20.launcher2.calendar.CalendarEventDeserializer -import de.mm20.launcher2.calendar.CalendarEventSerializer -import de.mm20.launcher2.contacts.ContactDeserializer -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 -import de.mm20.launcher2.search.data.* -import de.mm20.launcher2.websites.WebsiteDeserializer -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) + val serializer = getSerializer() return serializer.serialize(this) } - -internal fun getSerializer(searchable: Searchable?): SearchableSerializer { - if (searchable is LauncherApp) { - return LauncherAppSerializer() - } - if (searchable is LauncherShortcut) { - return LauncherShortcutSerializer() - } - if (searchable is LegacyShortcut) { - return LegacyShortcutSerializer() - } - if (searchable is CalendarEvent) { - return CalendarEventSerializer() - } - if (searchable is Contact) { - return ContactSerializer() - } - if (searchable is Wikipedia) { - return WikipediaSerializer() - } - if (searchable is GDriveFile) { - return GDriveFileSerializer() - } - if (searchable is OneDriveFile) { - return OneDriveFileSerializer() - } - if (searchable is OwncloudFile) { - return OwncloudFileSerializer() - } - if (searchable is NextcloudFile) { - return NextcloudFileSerializer() - } - if (searchable is LocalFile) { - return LocalFileSerializer() - } - if (searchable is Website) { - return WebsiteSerializer() - } - if (searchable is Tag) { - return TagSerializer() - } - return NullSerializer() -} - -internal fun getDeserializer(context: Context, type: String): SearchableDeserializer { - if (type == LauncherApp.Domain) { - return LauncherAppDeserializer(context) - } - if (type == LauncherShortcut.Domain) { - return LauncherShortcutDeserializer(context) - } - if (type == LegacyShortcut.Domain) { - return LegacyShortcutDeserializer(context) - } - if (type == CalendarEvent.Domain) { - return CalendarEventDeserializer(context) - } - if (type == Contact.Domain) { - return ContactDeserializer(context) - } - if (type == Wikipedia.Domain) { - return WikipediaDeserializer(context) - } - if (type == GDriveFile.Domain) { - return GDriveFileDeserializer() - } - if (type == OneDriveFile.Domain) { - return OneDriveFileDeserializer() - } - if (type == NextcloudFile.Domain) { - return NextcloudFileDeserializer() - } - if (type == OwncloudFile.Domain) { - return OwncloudFileDeserializer() - } - if (type == LocalFile.Domain) { - return LocalFileDeserializer(context) - } - if (type == Website.Domain) { - return WebsiteDeserializer() - } - if (type == Tag.Domain) { - return TagDeserializer() - } - return NullDeserializer() -} \ No newline at end of file diff --git a/data/searchable/src/main/java/de/mm20/launcher2/searchable/TagSerialization.kt b/data/searchable/src/main/java/de/mm20/launcher2/searchable/TagSerialization.kt index 0e824148..4931f39a 100644 --- a/data/searchable/src/main/java/de/mm20/launcher2/searchable/TagSerialization.kt +++ b/data/searchable/src/main/java/de/mm20/launcher2/searchable/TagSerialization.kt @@ -19,7 +19,7 @@ class TagSerializer: SearchableSerializer { } class TagDeserializer: SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable { + override suspend fun deserialize(serialized: String): SavableSearchable { val json = JSONObject(serialized) return Tag(json.getString("tag")) diff --git a/data/websites/src/main/java/de/mm20/launcher2/websites/Module.kt b/data/websites/src/main/java/de/mm20/launcher2/websites/Module.kt index cbf71d88..1a276cf3 100644 --- a/data/websites/src/main/java/de/mm20/launcher2/websites/Module.kt +++ b/data/websites/src/main/java/de/mm20/launcher2/websites/Module.kt @@ -1,8 +1,13 @@ package de.mm20.launcher2.websites +import de.mm20.launcher2.search.SearchableDeserializer +import de.mm20.launcher2.search.SearchableRepository +import de.mm20.launcher2.search.Website import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val websitesModule = module { - single { WebsiteRepositoryImpl(androidContext()) } + single>(named()) { WebsiteRepository(androidContext()) } + factory(named(WebsiteImpl.Domain)) { WebsiteDeserializer() } } \ No newline at end of file diff --git a/data/websites/src/main/java/de/mm20/launcher2/search/data/Website.kt b/data/websites/src/main/java/de/mm20/launcher2/websites/Website.kt similarity index 81% rename from data/websites/src/main/java/de/mm20/launcher2/search/data/Website.kt rename to data/websites/src/main/java/de/mm20/launcher2/websites/Website.kt index 8bc4922b..436a560d 100644 --- a/data/websites/src/main/java/de/mm20/launcher2/search/data/Website.kt +++ b/data/websites/src/main/java/de/mm20/launcher2/websites/Website.kt @@ -1,4 +1,4 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.websites import android.content.Context import android.content.Intent @@ -9,19 +9,19 @@ import coil.imageLoader import coil.request.ImageRequest import de.mm20.launcher2.icons.* import de.mm20.launcher2.ktx.tryStartActivity -import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.websites.R +import de.mm20.launcher2.search.SearchableSerializer +import de.mm20.launcher2.search.Website import java.util.concurrent.ExecutionException -data class Website( +internal data class WebsiteImpl( override val label: String, - val url: String, - val description: String, - val image: String, - val favicon: String, - val color: Int, + override val url: String, + override val description: String, + override val imageUrl: String, + override val faviconUrl: String, + override val color: Int, override val labelOverride: String? = null, -) : SavableSearchable { +) : Website { override val domain: String = Domain @@ -38,10 +38,10 @@ data class Website( size: Int, themed: Boolean, ): LauncherIcon? { - if (favicon.isEmpty()) return null + if (faviconUrl.isEmpty()) return null try { val request = ImageRequest.Builder(context) - .data(favicon) + .data(faviconUrl) .size(size) .allowHardware(false) .build() @@ -90,7 +90,9 @@ data class Website( return context.tryStartActivity(getLaunchIntent(), options) } - fun share(context: Context) { + override val canShare: Boolean = true + + override fun share(context: Context) { val shareIntent = Intent(Intent.ACTION_SEND) shareIntent.putExtra( Intent.EXTRA_TEXT, @@ -100,6 +102,10 @@ data class Website( context.startActivity(Intent.createChooser(shareIntent, null)) } + override fun getSerializer(): SearchableSerializer { + return WebsiteSerializer() + } + companion object { const val Domain = "web" } diff --git a/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt b/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt index e2fab7e2..951e53ff 100644 --- a/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt +++ b/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteRepository.kt @@ -2,8 +2,12 @@ package de.mm20.launcher2.websites import android.content.Context import android.webkit.URLUtil +import androidx.compose.runtime.Immutable import androidx.core.graphics.toColorInt -import de.mm20.launcher2.search.data.Website +import de.mm20.launcher2.search.SearchableRepository +import de.mm20.launcher2.search.Website +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow @@ -20,11 +24,8 @@ import java.net.URISyntaxException import java.net.URL import java.util.concurrent.TimeUnit -interface WebsiteRepository { - fun search(query: String): Flow -} -internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository, KoinComponent { +internal class WebsiteRepository(val context: Context) : SearchableRepository { private val httpClient = OkHttpClient .Builder() @@ -33,16 +34,17 @@ internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository, .writeTimeout(1000, TimeUnit.MILLISECONDS) .build() - override fun search(query: String): Flow = channelFlow { - send(null) + override fun search(query: String): Flow> = channelFlow { + send(persistentListOf()) withContext(Dispatchers.IO) { httpClient.dispatcher.cancelAll() } if (query.isBlank()) return@channelFlow val website = queryWebsite(query) - send(website) - + website?.let { + send(persistentListOf(it)) + } } private suspend fun queryWebsite(query: String): Website? { @@ -84,12 +86,12 @@ internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository, doc.head().select("link[href~=.*\\.(ico|png)]").attr("href") if (favicon.isNotBlank()) favicon = resolveUrl(response.request.url, favicon) if (image.isNotBlank()) image = resolveUrl(response.request.url, image) - return@withContext Website( + return@withContext WebsiteImpl( label = title, url = url, description = description, - image = image, - favicon = favicon, + imageUrl = image, + faviconUrl = favicon, color = color ) } catch (e: IOException) { diff --git a/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteSerialization.kt b/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteSerialization.kt index 002a998b..31093033 100644 --- a/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteSerialization.kt +++ b/data/websites/src/main/java/de/mm20/launcher2/websites/WebsiteSerialization.kt @@ -4,18 +4,17 @@ import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableSerializer -import de.mm20.launcher2.search.data.Website import org.json.JSONObject class WebsiteSerializer : SearchableSerializer { override fun serialize(searchable: SavableSearchable): String { - searchable as Website + searchable as WebsiteImpl return jsonObjectOf( "label" to searchable.label, "url" to searchable.url, "description" to searchable.description, - "image" to searchable.image, - "favicon" to searchable.favicon, + "image" to searchable.imageUrl, + "favicon" to searchable.faviconUrl, "color" to searchable.color ).toString() } @@ -25,12 +24,12 @@ class WebsiteSerializer : SearchableSerializer { } class WebsiteDeserializer: SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable? { + override suspend fun deserialize(serialized: String): SavableSearchable? { val json = JSONObject(serialized) - return Website( + return WebsiteImpl( label = json.getString("label"), - favicon = json.getString("favicon"), - image = json.getString("image"), + faviconUrl = json.getString("favicon"), + imageUrl = json.getString("image"), description = json.getString("description"), url = json.getString("url"), color = json.getInt("color") diff --git a/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Module.kt b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Module.kt index fa8e2414..a07b1ad1 100644 --- a/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Module.kt +++ b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Module.kt @@ -1,8 +1,13 @@ package de.mm20.launcher2.wikipedia +import de.mm20.launcher2.search.Article +import de.mm20.launcher2.search.SearchableDeserializer +import de.mm20.launcher2.search.SearchableRepository import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val wikipediaModule = module { - single { WikipediaRepositoryImpl(androidContext(), get()) } + single>(named
()) { WikipediaRepository(androidContext(), get()) } + factory(named(Wikipedia.Domain)) { WikipediaDeserializer(androidContext()) } } \ No newline at end of file diff --git a/data/wikipedia/src/main/java/de/mm20/launcher2/search/data/Wikipedia.kt b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Wikipedia.kt similarity index 75% rename from data/wikipedia/src/main/java/de/mm20/launcher2/search/data/Wikipedia.kt rename to data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Wikipedia.kt index 6fc2b4b6..8e37620a 100644 --- a/data/wikipedia/src/main/java/de/mm20/launcher2/search/data/Wikipedia.kt +++ b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/Wikipedia.kt @@ -1,34 +1,31 @@ -package de.mm20.launcher2.search.data +package de.mm20.launcher2.wikipedia import android.content.Context import android.content.Intent -import android.graphics.Color import android.net.Uri import android.os.Bundle -import androidx.browser.customtabs.CustomTabsIntent import androidx.core.content.ContextCompat import androidx.core.text.HtmlCompat import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.TintedIconLayer import de.mm20.launcher2.ktx.tryStartActivity -import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.wikipedia.R +import de.mm20.launcher2.search.Article +import de.mm20.launcher2.search.SearchableSerializer -data class Wikipedia( +internal data class Wikipedia( override val label: String, val id: Long, - val text: String, - val image: String?, - val url: String, + override val text: String, + override val imageUrl: String?, + override val sourceUrl: String, + override val sourceName: String, val wikipediaUrl: String, override val labelOverride: String? = null, -) : SavableSearchable { +) : Article { override val domain: String = Domain - override val preferDetailsOverLaunch: Boolean = false - override fun overrideLabel(label: String): Wikipedia { return this.copy(labelOverride = label) } @@ -47,25 +44,30 @@ data class Wikipedia( } private fun getLaunchIntent(): Intent { - return Intent(Intent.ACTION_VIEW, Uri.parse(url)) + return Intent(Intent.ACTION_VIEW, Uri.parse(sourceUrl)) } override fun launch(context: Context, options: Bundle?): Boolean { return context.tryStartActivity(getLaunchIntent(), options) } - fun share(context: Context) { + override val canShare: Boolean = true + override fun share(context: Context) { val text = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() val shareIntent = Intent(Intent.ACTION_SEND) shareIntent.putExtra( Intent.EXTRA_TEXT, "${label}\n\n" + "${text.substring(0, 200)}…\n\n" + - url + sourceUrl ) shareIntent.type = "text/plain" context.startActivity(Intent.createChooser(shareIntent, null)) } + override fun getSerializer(): SearchableSerializer { + return WikipediaSerializer() + } + companion object { const val Domain = "wikipedia" } diff --git a/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt index 3ad030f8..db535ab6 100644 --- a/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt +++ b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaRepository.kt @@ -3,7 +3,10 @@ package de.mm20.launcher2.wikipedia import android.content.Context import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.preferences.LauncherDataStore -import de.mm20.launcher2.search.data.Wikipedia +import de.mm20.launcher2.search.Article +import de.mm20.launcher2.search.SearchableRepository +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import okhttp3.OkHttpClient @@ -12,14 +15,11 @@ import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit -interface WikipediaRepository { - fun search(query: String, loadImages: Boolean = false): Flow -} -internal class WikipediaRepositoryImpl( +internal class WikipediaRepository( private val context: Context, private val dataStore: LauncherDataStore -) : WikipediaRepository, KoinComponent { +) : SearchableRepository
{ private val scope = CoroutineScope(Job() + Dispatchers.Default) @@ -55,8 +55,8 @@ internal class WikipediaRepositoryImpl( private lateinit var wikipediaService: WikipediaApi - override fun search(query: String, loadImages: Boolean): Flow = channelFlow { - send(null) + override fun search(query: String): Flow> = channelFlow { + send(persistentListOf()) withContext(Dispatchers.IO) { httpClient.dispatcher.cancelAll() } @@ -66,7 +66,10 @@ internal class WikipediaRepositoryImpl( if (!::wikipediaService.isInitialized) return@channelFlow if (query.isBlank()) return@channelFlow - send(queryWikipedia(query, loadImages)) + dataStore.data.map { it.wikipediaSearch.images }.collectLatest { + val wikipedia = queryWikipedia(query, false) + send(wikipedia?.let { persistentListOf(it) } ?: persistentListOf()) + } } private suspend fun queryWikipedia(query: String, loadImages: Boolean): Wikipedia? { @@ -92,9 +95,10 @@ internal class WikipediaRepositoryImpl( label = page.title, id = page.pageid, text = page.extract, - image = image, - url = page.fullurl, - wikipediaUrl = wikipediaUrl + imageUrl = image, + sourceUrl = page.fullurl, + wikipediaUrl = wikipediaUrl, + sourceName = context.getString(R.string.wikipedia_source), ) } diff --git a/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaSerialization.kt b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaSerialization.kt index 72a311bd..d36cb30d 100644 --- a/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaSerialization.kt +++ b/data/wikipedia/src/main/java/de/mm20/launcher2/wikipedia/WikipediaSerialization.kt @@ -4,7 +4,6 @@ import android.content.Context import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableSerializer -import de.mm20.launcher2.search.data.Wikipedia import org.json.JSONObject class WikipediaSerializer : SearchableSerializer { @@ -14,9 +13,9 @@ class WikipediaSerializer : SearchableSerializer { json.put("label", searchable.label) json.put("text", searchable.text) json.put("id", searchable.id) - json.put("image", searchable.image) + json.put("image", searchable.imageUrl) json.put("wikipedia_url", searchable.wikipediaUrl) - json.put("url", searchable.url) + json.put("url", searchable.sourceUrl) return json.toString() } @@ -25,7 +24,7 @@ class WikipediaSerializer : SearchableSerializer { } class WikipediaDeserializer(val context: Context) : SearchableDeserializer { - override fun deserialize(serialized: String): SavableSearchable? { + override suspend fun deserialize(serialized: String): SavableSearchable? { val json = JSONObject(serialized) val wikipediaUrl = json.optString("wikipedia_url").takeIf { !it.isNullOrBlank() } ?: return null val id = json.getLong("id") @@ -33,9 +32,10 @@ class WikipediaDeserializer(val context: Context) : SearchableDeserializer { label = json.getString("label"), text = json.getString("text"), id = id, - image = json.optString("image"), - url = json.optString("url").takeIf { !it.isNullOrBlank() } ?: "${wikipediaUrl.padEnd(1, '/')}wiki?curid=$id", - wikipediaUrl = wikipediaUrl + imageUrl = json.optString("image"), + sourceUrl = json.optString("url").takeIf { !it.isNullOrBlank() } ?: "${wikipediaUrl.padEnd(1, '/')}wiki?curid=$id", + wikipediaUrl = wikipediaUrl, + sourceName = context.getString(R.string.wikipedia_source), ) } } \ No newline at end of file diff --git a/docs/static/img/dependency-graph.dot b/docs/static/img/dependency-graph.dot index b504a77f..feec16e1 100644 --- a/docs/static/img/dependency-graph.dot +++ b/docs/static/img/dependency-graph.dot @@ -15,6 +15,7 @@ digraph { ":core:ktx" [fillcolor="#94c1ff"]; ":core:permissions" [fillcolor="#94c1ff"]; ":core:preferences" [fillcolor="#94c1ff"]; + ":core:shared" [fillcolor="#94c1ff"]; ":data:applications" [fillcolor="#fff694"]; ":data:appshortcuts" [fillcolor="#fff694"]; ":data:calculator" [fillcolor="#fff694"]; @@ -22,10 +23,11 @@ digraph { ":data:contacts" [fillcolor="#fff694"]; ":data:currencies" [fillcolor="#fff694"]; ":data:customattrs" [fillcolor="#fff694"]; - ":data:favorites" [fillcolor="#fff694"]; ":data:files" [fillcolor="#fff694"]; ":data:notifications" [fillcolor="#fff694"]; ":data:search-actions" [fillcolor="#fff694"]; + ":data:searchable" [fillcolor="#fff694"]; + ":data:themes" [fillcolor="#fff694"]; ":data:unitconverter" [fillcolor="#fff694"]; ":data:weather" [fillcolor="#fff694"]; ":data:websites" [fillcolor="#fff694"]; @@ -37,9 +39,11 @@ digraph { ":libs:nextcloud" [fillcolor="#ad94ff"]; ":libs:owncloud" [fillcolor="#ad94ff"]; ":libs:webdav" [fillcolor="#ad94ff"]; + ":plugins:sdk" []; ":services:accounts" [fillcolor="#ff9498"]; ":services:backup" [fillcolor="#ff9498"]; ":services:badges" [fillcolor="#ff9498"]; + ":services:favorites" [fillcolor="#ff9498"]; ":services:global-actions" [fillcolor="#ff9498"]; ":services:icons" [fillcolor="#ff9498"]; ":services:music" [fillcolor="#ff9498"]; @@ -64,13 +68,13 @@ digraph { ":app:app" -> ":core:crashreporter" [style=dotted] ":app:app" -> ":data:currencies" [style=dotted] ":app:app" -> ":data:customattrs" [style=dotted] - ":app:app" -> ":data:favorites" [style=dotted] + ":app:app" -> ":data:searchable" [style=dotted] + ":app:app" -> ":data:themes" [style=dotted] ":app:app" -> ":data:files" [style=dotted] ":app:app" -> ":libs:g-services" [style=dotted] ":app:app" -> ":core:i18n" [style=dotted] ":app:app" -> ":services:icons" [style=dotted] ":app:app" -> ":core:ktx" [style=dotted] - ":app:app" -> ":libs:ms-services" [style=dotted] ":app:app" -> ":services:music" [style=dotted] ":app:app" -> ":libs:nextcloud" [style=dotted] ":app:app" -> ":data:notifications" [style=dotted] @@ -89,6 +93,7 @@ digraph { ":app:app" -> ":data:search-actions" [style=dotted] ":app:app" -> ":services:global-actions" [style=dotted] ":app:app" -> ":services:widgets" [style=dotted] + ":app:app" -> ":services:favorites" [style=dotted] ":app:ui" -> ":app:ui" ":app:ui" -> ":libs:material-color-utilities" [style=dotted] ":app:ui" -> ":core:base" [style=dotted] @@ -107,7 +112,8 @@ digraph { ":app:ui" -> ":data:calculator" [style=dotted] ":app:ui" -> ":data:files" [style=dotted] ":app:ui" -> ":data:widgets" [style=dotted] - ":app:ui" -> ":data:favorites" [style=dotted] + ":app:ui" -> ":data:searchable" [style=dotted] + ":app:ui" -> ":data:themes" [style=dotted] ":app:ui" -> ":data:wikipedia" [style=dotted] ":app:ui" -> ":services:badges" [style=dotted] ":app:ui" -> ":core:crashreporter" [style=dotted] @@ -118,112 +124,50 @@ digraph { ":app:ui" -> ":data:unitconverter" [style=dotted] ":app:ui" -> ":libs:nextcloud" [style=dotted] ":app:ui" -> ":libs:g-services" [style=dotted] - ":app:ui" -> ":libs:ms-services" [style=dotted] ":app:ui" -> ":libs:owncloud" [style=dotted] ":app:ui" -> ":services:accounts" [style=dotted] ":app:ui" -> ":services:backup" [style=dotted] ":app:ui" -> ":data:search-actions" [style=dotted] ":app:ui" -> ":services:global-actions" [style=dotted] ":app:ui" -> ":services:widgets" [style=dotted] - ":core:base" -> ":core:base" - ":core:base" -> ":core:ktx" [style=dotted] - ":core:base" -> ":core:i18n" [style=dotted] - ":core:compat" -> ":core:compat" - ":core:crashreporter" -> ":core:crashreporter" - ":core:crashreporter" -> ":core:base" [style=dotted] + ":app:ui" -> ":services:favorites" [style=dotted] + ":core:shared" -> ":core:shared" ":core:database" -> ":core:database" ":core:database" -> ":core:i18n" [style=dotted] ":core:database" -> ":core:ktx" [style=dotted] - ":core:i18n" -> ":core:i18n" - ":core:ktx" -> ":core:ktx" - ":core:permissions" -> ":core:permissions" - ":core:permissions" -> ":core:ktx" [style=dotted] - ":core:permissions" -> ":core:base" [style=dotted] - ":core:permissions" -> ":core:crashreporter" [style=dotted] + ":core:database" -> ":core:preferences" [style=dotted] ":core:preferences" -> ":core:preferences" ":core:preferences" -> ":core:ktx" [style=dotted] ":core:preferences" -> ":core:i18n" [style=dotted] ":core:preferences" -> ":core:base" [style=dotted] ":core:preferences" -> ":core:crashreporter" [style=dotted] ":core:preferences" -> ":libs:material-color-utilities" [style=dotted] - ":data:applications" -> ":data:applications" - ":data:applications" -> ":core:base" [style=dotted] - ":data:applications" -> ":core:ktx" [style=dotted] - ":data:applications" -> ":core:compat" [style=dotted] - ":data:appshortcuts" -> ":data:appshortcuts" - ":data:appshortcuts" -> ":data:applications" [style=dotted] - ":data:appshortcuts" -> ":core:permissions" [style=dotted] - ":data:appshortcuts" -> ":core:base" [style=dotted] - ":data:appshortcuts" -> ":core:ktx" [style=dotted] - ":data:calculator" -> ":data:calculator" - ":data:calculator" -> ":core:base" [style=dotted] + ":core:permissions" -> ":core:permissions" + ":core:permissions" -> ":core:ktx" [style=dotted] + ":core:permissions" -> ":core:base" [style=dotted] + ":core:permissions" -> ":core:crashreporter" [style=dotted] + ":core:compat" -> ":core:compat" + ":core:crashreporter" -> ":core:crashreporter" + ":core:crashreporter" -> ":core:base" [style=dotted] + ":core:i18n" -> ":core:i18n" + ":core:ktx" -> ":core:ktx" + ":core:base" -> ":core:base" + ":core:base" -> ":core:ktx" [style=dotted] + ":core:base" -> ":core:i18n" [style=dotted] + ":core:base" -> ":libs:material-color-utilities" [style=dotted] ":data:calendar" -> ":data:calendar" ":data:calendar" -> ":core:ktx" [style=dotted] ":data:calendar" -> ":core:base" [style=dotted] ":data:calendar" -> ":core:permissions" [style=dotted] ":data:calendar" -> ":libs:material-color-utilities" [style=dotted] - ":data:contacts" -> ":data:contacts" - ":data:contacts" -> ":core:ktx" [style=dotted] - ":data:contacts" -> ":core:base" [style=dotted] - ":data:contacts" -> ":core:permissions" [style=dotted] - ":data:currencies" -> ":data:currencies" - ":data:currencies" -> ":core:ktx" [style=dotted] - ":data:currencies" -> ":core:i18n" [style=dotted] - ":data:currencies" -> ":core:database" [style=dotted] - ":data:currencies" -> ":core:crashreporter" [style=dotted] - ":data:customattrs" -> ":data:customattrs" - ":data:customattrs" -> ":core:database" [style=dotted] - ":data:customattrs" -> ":core:base" [style=dotted] - ":data:customattrs" -> ":core:ktx" [style=dotted] - ":data:customattrs" -> ":core:crashreporter" [style=dotted] - ":data:customattrs" -> ":data:favorites" [style=dotted] - ":data:favorites" -> ":data:favorites" - ":data:favorites" -> ":core:base" [style=dotted] - ":data:favorites" -> ":data:calendar" [style=dotted] - ":data:favorites" -> ":core:database" [style=dotted] - ":data:favorites" -> ":core:preferences" [style=dotted] - ":data:favorites" -> ":data:applications" [style=dotted] - ":data:favorites" -> ":data:appshortcuts" [style=dotted] - ":data:favorites" -> ":data:contacts" [style=dotted] - ":data:favorites" -> ":core:ktx" [style=dotted] - ":data:favorites" -> ":data:files" [style=dotted] - ":data:favorites" -> ":data:websites" [style=dotted] - ":data:favorites" -> ":data:wikipedia" [style=dotted] - ":data:favorites" -> ":services:badges" [style=dotted] - ":data:favorites" -> ":core:crashreporter" [style=dotted] - ":data:files" -> ":data:files" - ":data:files" -> ":core:base" [style=dotted] - ":data:files" -> ":core:ktx" [style=dotted] - ":data:files" -> ":libs:ms-services" [style=dotted] - ":data:files" -> ":libs:g-services" [style=dotted] - ":data:files" -> ":libs:nextcloud" [style=dotted] - ":data:files" -> ":libs:owncloud" [style=dotted] - ":data:files" -> ":core:i18n" [style=dotted] - ":data:files" -> ":core:permissions" [style=dotted] - ":data:files" -> ":core:crashreporter" [style=dotted] - ":data:notifications" -> ":data:notifications" - ":data:notifications" -> ":core:permissions" [style=dotted] - ":data:search-actions" -> ":data:search-actions" - ":data:search-actions" -> ":core:base" [style=dotted] - ":data:search-actions" -> ":core:database" [style=dotted] - ":data:search-actions" -> ":core:ktx" [style=dotted] - ":data:search-actions" -> ":core:preferences" [style=dotted] - ":data:search-actions" -> ":core:crashreporter" [style=dotted] - ":data:unitconverter" -> ":data:unitconverter" - ":data:unitconverter" -> ":core:preferences" [style=dotted] - ":data:unitconverter" -> ":data:currencies" [style=dotted] - ":data:unitconverter" -> ":core:base" [style=dotted] - ":data:unitconverter" -> ":core:i18n" [style=dotted] - ":data:weather" -> ":data:weather" - ":data:weather" -> ":core:database" [style=dotted] - ":data:weather" -> ":core:ktx" [style=dotted] - ":data:weather" -> ":core:crashreporter" [style=dotted] - ":data:weather" -> ":core:preferences" [style=dotted] - ":data:weather" -> ":core:permissions" [style=dotted] - ":data:weather" -> ":core:i18n" [style=dotted] - ":data:websites" -> ":data:websites" - ":data:websites" -> ":core:base" [style=dotted] - ":data:websites" -> ":core:ktx" [style=dotted] + ":data:calculator" -> ":data:calculator" + ":data:calculator" -> ":core:base" [style=dotted] + ":data:appshortcuts" -> ":data:appshortcuts" + ":data:appshortcuts" -> ":data:applications" [style=dotted] + ":data:appshortcuts" -> ":core:permissions" [style=dotted] + ":data:appshortcuts" -> ":core:base" [style=dotted] + ":data:appshortcuts" -> ":core:ktx" [style=dotted] + ":data:appshortcuts" -> ":core:crashreporter" [style=dotted] ":data:widgets" -> ":data:widgets" ":data:widgets" -> ":data:weather" [style=dotted] ":data:widgets" -> ":data:calendar" [style=dotted] @@ -233,40 +177,79 @@ digraph { ":data:widgets" -> ":core:preferences" [style=dotted] ":data:widgets" -> ":core:database" [style=dotted] ":data:widgets" -> ":core:crashreporter" [style=dotted] + ":data:searchable" -> ":data:searchable" + ":data:searchable" -> ":core:base" [style=dotted] + ":data:searchable" -> ":data:calendar" [style=dotted] + ":data:searchable" -> ":core:database" [style=dotted] + ":data:searchable" -> ":core:preferences" [style=dotted] + ":data:searchable" -> ":core:ktx" [style=dotted] + ":data:searchable" -> ":data:wikipedia" [style=dotted] + ":data:searchable" -> ":services:badges" [style=dotted] + ":data:searchable" -> ":core:crashreporter" [style=dotted] + ":data:unitconverter" -> ":data:unitconverter" + ":data:unitconverter" -> ":core:preferences" [style=dotted] + ":data:unitconverter" -> ":data:currencies" [style=dotted] + ":data:unitconverter" -> ":core:base" [style=dotted] + ":data:unitconverter" -> ":core:i18n" [style=dotted] + ":data:themes" -> ":data:themes" + ":data:themes" -> ":core:base" [style=dotted] + ":data:themes" -> ":core:database" [style=dotted] + ":data:themes" -> ":core:crashreporter" [style=dotted] + ":data:themes" -> ":libs:material-color-utilities" [style=dotted] + ":data:customattrs" -> ":data:customattrs" + ":data:customattrs" -> ":core:database" [style=dotted] + ":data:customattrs" -> ":core:base" [style=dotted] + ":data:customattrs" -> ":core:ktx" [style=dotted] + ":data:customattrs" -> ":core:crashreporter" [style=dotted] + ":data:customattrs" -> ":data:searchable" [style=dotted] + ":data:weather" -> ":data:weather" + ":data:weather" -> ":core:database" [style=dotted] + ":data:weather" -> ":core:ktx" [style=dotted] + ":data:weather" -> ":core:crashreporter" [style=dotted] + ":data:weather" -> ":core:preferences" [style=dotted] + ":data:weather" -> ":core:permissions" [style=dotted] + ":data:weather" -> ":core:i18n" [style=dotted] + ":data:files" -> ":data:files" + ":data:files" -> ":core:base" [style=dotted] + ":data:files" -> ":core:ktx" [style=dotted] + ":data:files" -> ":libs:g-services" [style=dotted] + ":data:files" -> ":libs:nextcloud" [style=dotted] + ":data:files" -> ":libs:owncloud" [style=dotted] + ":data:files" -> ":core:i18n" [style=dotted] + ":data:files" -> ":core:permissions" [style=dotted] + ":data:files" -> ":core:crashreporter" [style=dotted] + ":data:files" -> ":core:preferences" [style=dotted] + ":data:websites" -> ":data:websites" + ":data:websites" -> ":core:base" [style=dotted] + ":data:websites" -> ":core:ktx" [style=dotted] + ":data:search-actions" -> ":data:search-actions" + ":data:search-actions" -> ":core:base" [style=dotted] + ":data:search-actions" -> ":core:database" [style=dotted] + ":data:search-actions" -> ":core:ktx" [style=dotted] + ":data:search-actions" -> ":core:preferences" [style=dotted] + ":data:search-actions" -> ":core:crashreporter" [style=dotted] ":data:wikipedia" -> ":data:wikipedia" ":data:wikipedia" -> ":core:preferences" [style=dotted] ":data:wikipedia" -> ":core:base" [style=dotted] ":data:wikipedia" -> ":core:ktx" [style=dotted] ":data:wikipedia" -> ":core:crashreporter" [style=dotted] - ":libs:g-services" -> ":libs:g-services" - ":libs:g-services" -> ":core:i18n" [style=dotted] - ":libs:g-services" -> ":core:crashreporter" [style=dotted] - ":libs:material-color-utilities" -> ":libs:material-color-utilities" - ":libs:ms-services" -> ":libs:ms-services" - ":libs:ms-services" -> ":core:crashreporter" [style=dotted] - ":libs:nextcloud" -> ":libs:webdav" - ":libs:nextcloud" -> ":libs:nextcloud" - ":libs:nextcloud" -> ":core:i18n" [style=dotted] - ":libs:owncloud" -> ":libs:webdav" - ":libs:owncloud" -> ":libs:owncloud" - ":libs:owncloud" -> ":core:crashreporter" [style=dotted] - ":libs:owncloud" -> ":core:ktx" [style=dotted] - ":libs:owncloud" -> ":core:i18n" [style=dotted] - ":libs:webdav" -> ":libs:webdav" - ":libs:webdav" -> ":core:crashreporter" [style=dotted] - ":libs:webdav" -> ":core:ktx" [style=dotted] - ":services:accounts" -> ":services:accounts" - ":services:accounts" -> ":libs:g-services" [style=dotted] - ":services:accounts" -> ":libs:ms-services" [style=dotted] - ":services:accounts" -> ":libs:owncloud" [style=dotted] - ":services:accounts" -> ":libs:nextcloud" [style=dotted] - ":services:backup" -> ":services:backup" - ":services:backup" -> ":data:favorites" [style=dotted] - ":services:backup" -> ":data:widgets" [style=dotted] - ":services:backup" -> ":data:search-actions" [style=dotted] - ":services:backup" -> ":core:preferences" [style=dotted] - ":services:backup" -> ":core:ktx" [style=dotted] - ":services:backup" -> ":data:customattrs" [style=dotted] + ":data:contacts" -> ":data:contacts" + ":data:contacts" -> ":core:ktx" [style=dotted] + ":data:contacts" -> ":core:base" [style=dotted] + ":data:contacts" -> ":core:permissions" [style=dotted] + ":data:notifications" -> ":data:notifications" + ":data:notifications" -> ":core:permissions" [style=dotted] + ":data:applications" -> ":data:applications" + ":data:applications" -> ":core:base" [style=dotted] + ":data:applications" -> ":core:ktx" [style=dotted] + ":data:applications" -> ":core:compat" [style=dotted] + ":data:currencies" -> ":data:currencies" + ":data:currencies" -> ":core:ktx" [style=dotted] + ":data:currencies" -> ":core:i18n" [style=dotted] + ":data:currencies" -> ":core:database" [style=dotted] + ":data:currencies" -> ":core:crashreporter" [style=dotted] + ":plugins:sdk" -> ":plugins:sdk" + ":plugins:sdk" -> ":core:shared" [style=dotted] ":services:badges" -> ":services:badges" ":services:badges" -> ":core:ktx" [style=dotted] ":services:badges" -> ":data:applications" [style=dotted] @@ -275,11 +258,36 @@ digraph { ":services:badges" -> ":core:preferences" [style=dotted] ":services:badges" -> ":core:base" [style=dotted] ":services:badges" -> ":data:files" [style=dotted] - ":services:global-actions" -> ":services:global-actions" - ":services:global-actions" -> ":core:preferences" [style=dotted] - ":services:global-actions" -> ":core:base" [style=dotted] - ":services:global-actions" -> ":core:i18n" [style=dotted] - ":services:global-actions" -> ":core:permissions" [style=dotted] + ":services:favorites" -> ":services:favorites" + ":services:favorites" -> ":core:base" [style=dotted] + ":services:favorites" -> ":core:i18n" [style=dotted] + ":services:favorites" -> ":data:searchable" [style=dotted] + ":services:backup" -> ":services:backup" + ":services:backup" -> ":data:searchable" [style=dotted] + ":services:backup" -> ":data:widgets" [style=dotted] + ":services:backup" -> ":data:search-actions" [style=dotted] + ":services:backup" -> ":core:preferences" [style=dotted] + ":services:backup" -> ":core:ktx" [style=dotted] + ":services:backup" -> ":data:customattrs" [style=dotted] + ":services:backup" -> ":data:themes" [style=dotted] + ":services:search" -> ":services:search" + ":services:search" -> ":data:calculator" [style=dotted] + ":services:search" -> ":data:unitconverter" [style=dotted] + ":services:search" -> ":data:customattrs" [style=dotted] + ":services:search" -> ":data:search-actions" [style=dotted] + ":services:search" -> ":core:base" [style=dotted] + ":services:search" -> ":core:preferences" [style=dotted] + ":services:search" -> ":core:crashreporter" [style=dotted] + ":services:search" -> ":core:ktx" [style=dotted] + ":services:music" -> ":services:music" + ":services:music" -> ":core:ktx" [style=dotted] + ":services:music" -> ":core:preferences" [style=dotted] + ":services:music" -> ":data:notifications" [style=dotted] + ":services:music" -> ":core:crashreporter" [style=dotted] + ":services:accounts" -> ":services:accounts" + ":services:accounts" -> ":libs:g-services" [style=dotted] + ":services:accounts" -> ":libs:owncloud" [style=dotted] + ":services:accounts" -> ":libs:nextcloud" [style=dotted] ":services:icons" -> ":data:customattrs" ":services:icons" -> ":services:icons" ":services:icons" -> ":core:database" [style=dotted] @@ -288,37 +296,37 @@ digraph { ":services:icons" -> ":core:base" [style=dotted] ":services:icons" -> ":data:applications" [style=dotted] ":services:icons" -> ":core:crashreporter" [style=dotted] - ":services:music" -> ":services:music" - ":services:music" -> ":core:ktx" [style=dotted] - ":services:music" -> ":core:preferences" [style=dotted] - ":services:music" -> ":data:notifications" [style=dotted] - ":services:music" -> ":core:crashreporter" [style=dotted] - ":services:search" -> ":services:search" - ":services:search" -> ":data:applications" [style=dotted] - ":services:search" -> ":data:appshortcuts" [style=dotted] - ":services:search" -> ":data:calculator" [style=dotted] - ":services:search" -> ":data:calendar" [style=dotted] - ":services:search" -> ":data:contacts" [style=dotted] - ":services:search" -> ":data:files" [style=dotted] - ":services:search" -> ":data:unitconverter" [style=dotted] - ":services:search" -> ":data:websites" [style=dotted] - ":services:search" -> ":data:wikipedia" [style=dotted] - ":services:search" -> ":data:customattrs" [style=dotted] - ":services:search" -> ":data:search-actions" [style=dotted] - ":services:search" -> ":core:base" [style=dotted] - ":services:search" -> ":core:database" [style=dotted] - ":services:search" -> ":core:preferences" [style=dotted] - ":services:search" -> ":core:crashreporter" [style=dotted] - ":services:search" -> ":core:ktx" [style=dotted] + ":services:widgets" -> ":services:widgets" + ":services:widgets" -> ":core:base" [style=dotted] + ":services:widgets" -> ":core:i18n" [style=dotted] + ":services:widgets" -> ":data:widgets" [style=dotted] + ":services:global-actions" -> ":services:global-actions" + ":services:global-actions" -> ":core:preferences" [style=dotted] + ":services:global-actions" -> ":core:base" [style=dotted] + ":services:global-actions" -> ":core:i18n" [style=dotted] + ":services:global-actions" -> ":core:permissions" [style=dotted] ":services:tags" -> ":services:tags" ":services:tags" -> ":core:preferences" [style=dotted] ":services:tags" -> ":core:base" [style=dotted] ":services:tags" -> ":core:ktx" [style=dotted] ":services:tags" -> ":core:crashreporter" [style=dotted] ":services:tags" -> ":data:customattrs" [style=dotted] - ":services:tags" -> ":data:favorites" [style=dotted] - ":services:widgets" -> ":services:widgets" - ":services:widgets" -> ":core:base" [style=dotted] - ":services:widgets" -> ":core:i18n" [style=dotted] - ":services:widgets" -> ":data:widgets" [style=dotted] + ":services:tags" -> ":data:searchable" [style=dotted] + ":libs:nextcloud" -> ":libs:webdav" + ":libs:nextcloud" -> ":libs:nextcloud" + ":libs:nextcloud" -> ":core:i18n" [style=dotted] + ":libs:webdav" -> ":libs:webdav" + ":libs:webdav" -> ":core:crashreporter" [style=dotted] + ":libs:webdav" -> ":core:ktx" [style=dotted] + ":libs:g-services" -> ":libs:g-services" + ":libs:g-services" -> ":core:i18n" [style=dotted] + ":libs:g-services" -> ":core:crashreporter" [style=dotted] + ":libs:material-color-utilities" -> ":libs:material-color-utilities" + ":libs:owncloud" -> ":libs:webdav" + ":libs:owncloud" -> ":libs:owncloud" + ":libs:owncloud" -> ":core:crashreporter" [style=dotted] + ":libs:owncloud" -> ":core:ktx" [style=dotted] + ":libs:owncloud" -> ":core:i18n" [style=dotted] + ":libs:ms-services" -> ":libs:ms-services" + ":libs:ms-services" -> ":core:crashreporter" [style=dotted] } diff --git a/docs/static/img/dependency-graph.dot.png b/docs/static/img/dependency-graph.dot.png index a3957425..ff4ea8f5 100644 Binary files a/docs/static/img/dependency-graph.dot.png and b/docs/static/img/dependency-graph.dot.png differ 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 b765d4b6..5b37e969 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.searchable.SearchableRepository +import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.export import de.mm20.launcher2.preferences.import @@ -22,7 +22,7 @@ import java.util.zip.ZipOutputStream class BackupManager( private val context: Context, private val dataStore: LauncherDataStore, - private val searchableRepository: SearchableRepository, + private val searchableRepository: SavableSearchableRepository, private val widgetRepository: WidgetRepository, private val searchActionRepository: SearchActionRepository, private val customAttrsRepository: CustomAttributesRepository, diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt index 70f95874..127b5afd 100644 --- a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt @@ -4,10 +4,8 @@ import android.content.Context import android.content.pm.PackageManager import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.graphics.BadgeDrawable -import de.mm20.launcher2.search.data.LauncherShortcut -import de.mm20.launcher2.search.data.LegacyShortcut +import de.mm20.launcher2.search.AppShortcut import de.mm20.launcher2.search.Searchable -import de.mm20.launcher2.search.data.UnavailableShortcut import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow @@ -17,53 +15,37 @@ class AppShortcutBadgeProvider( private val context: Context ) : BadgeProvider { override fun getBadge(searchable: Searchable): Flow = channelFlow { - if (searchable is LauncherShortcut) { - val componentName = searchable.launcherShortcut.activity - if (componentName == null) { + if (searchable is AppShortcut) { + val componentName = searchable.componentName + val packageName = searchable.packageName + if (componentName != null) { + withContext(Dispatchers.IO) { + val icon = try { + context.packageManager.getActivityIcon( + componentName + ) + } catch (e: PackageManager.NameNotFoundException) { + return@withContext + } + val badge = Badge(icon = BadgeDrawable(context, icon)) + send(badge) + } + } else if (packageName != null) { + withContext(Dispatchers.IO) { + val icon = try { + context.packageManager.getApplicationIcon( + packageName + ) + } catch (e: PackageManager.NameNotFoundException) { + return@withContext + } + val badge = Badge(icon = BadgeDrawable(context, icon)) + send(badge) + } + } else { send(null) return@channelFlow } - withContext(Dispatchers.IO) { - val icon = try { - context.packageManager.getActivityIcon( - componentName - ) - } catch (e: PackageManager.NameNotFoundException) { - return@withContext - } - val badge = Badge(icon = BadgeDrawable(context, icon)) - send(badge) - } - } else if (searchable is LegacyShortcut) { - val packageName = searchable.packageName - if (packageName == null) { - send(null) - return@channelFlow - } - withContext(Dispatchers.IO) { - val icon = try { - context.packageManager.getApplicationIcon( - packageName - ) - } catch (e: PackageManager.NameNotFoundException) { - return@withContext - } - val badge = Badge(icon = BadgeDrawable(context, icon)) - send(badge) - } - } else if (searchable is UnavailableShortcut) { - val packageName = searchable.packageName - withContext(Dispatchers.IO) { - val icon = try { - context.packageManager.getApplicationIcon( - packageName - ) - } catch (e: PackageManager.NameNotFoundException) { - return@withContext - } - val badge = Badge(icon = BadgeDrawable(context, icon)) - send(badge) - } } else { send(null) } diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/CloudBadgeProvider.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/CloudBadgeProvider.kt index 28e3bcf5..cafffe20 100644 --- a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/CloudBadgeProvider.kt +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/CloudBadgeProvider.kt @@ -1,7 +1,7 @@ package de.mm20.launcher2.badges.providers import de.mm20.launcher2.badges.Badge -import de.mm20.launcher2.search.data.File +import de.mm20.launcher2.search.File import de.mm20.launcher2.search.Searchable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/NotificationBadgeProvider.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/NotificationBadgeProvider.kt index 855b899e..9bf7240b 100644 --- a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/NotificationBadgeProvider.kt +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/NotificationBadgeProvider.kt @@ -3,7 +3,7 @@ package de.mm20.launcher2.badges.providers import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.notifications.NotificationRepository import de.mm20.launcher2.search.Searchable -import de.mm20.launcher2.search.data.LauncherApp +import de.mm20.launcher2.search.Application import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest @@ -15,8 +15,8 @@ class NotificationBadgeProvider : BadgeProvider, KoinComponent { private val notificationRepository: NotificationRepository by inject() override fun getBadge(searchable: Searchable): Flow = channelFlow { - if (searchable is LauncherApp) { - val packageName = searchable.`package` + if (searchable is Application) { + val packageName = searchable.componentName.packageName notificationRepository.notifications.map { it.filter { it.packageName == packageName } }.collectLatest { diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/SuspendedAppsBadgeProvider.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/SuspendedAppsBadgeProvider.kt index cca49e86..a2c9710f 100644 --- a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/SuspendedAppsBadgeProvider.kt +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/SuspendedAppsBadgeProvider.kt @@ -1,33 +1,18 @@ package de.mm20.launcher2.badges.providers -import de.mm20.launcher2.applications.AppRepository import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.R +import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.Searchable -import de.mm20.launcher2.search.data.LauncherApp import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest import org.koin.core.component.KoinComponent -import org.koin.core.component.inject class SuspendedAppsBadgeProvider : BadgeProvider, KoinComponent { - private val appRepository: AppRepository by inject() override fun getBadge(searchable: Searchable): Flow = channelFlow { - if (searchable is LauncherApp) { - val packageName = searchable.`package` - appRepository.getSuspendedPackages().collectLatest { - if (it.contains(packageName)) { - send( - Badge( - iconRes = R.drawable.ic_badge_suspended - ) - ) - } else { - send(null) - } - } + if (searchable is Application && searchable.isSuspended) { + send(Badge(iconRes = R.drawable.ic_badge_suspended)) } else { send(null) } diff --git a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt index 6a0eb08f..99ed08ba 100644 --- a/services/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt +++ b/services/badges/src/main/java/de/mm20/launcher2/badges/providers/WorkProfileBadgeProvider.kt @@ -2,15 +2,16 @@ package de.mm20.launcher2.badges.providers import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.R -import de.mm20.launcher2.search.data.LauncherApp -import de.mm20.launcher2.search.data.LauncherShortcut +import de.mm20.launcher2.search.AppProfile +import de.mm20.launcher2.search.AppShortcut +import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.Searchable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class WorkProfileBadgeProvider : BadgeProvider { override fun getBadge(searchable: Searchable): Flow = flow { - if (searchable is LauncherApp && !searchable.isMainProfile || searchable is LauncherShortcut && !searchable.isMainProfile) { + if (searchable is Application && searchable.profile == AppProfile.Work || searchable is AppShortcut && searchable.profile == AppProfile.Work) { emit( Badge( iconRes = R.drawable.ic_badge_workprofile 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 index 8c5555b9..81f451a9 100644 --- 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 @@ -1,11 +1,11 @@ package de.mm20.launcher2.services.favorites import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.searchable.SearchableRepository +import de.mm20.launcher2.searchable.SavableSearchableRepository import kotlinx.coroutines.flow.Flow class FavoritesService( - val searchableRepository: SearchableRepository, + val searchableRepository: SavableSearchableRepository, ) { diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt index 15a403ee..9f741962 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt @@ -32,15 +32,14 @@ import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation import de.mm20.launcher2.icons.transformations.transform import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.preferences.LauncherDataStore +import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.LauncherApp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -283,9 +282,9 @@ class IconService( val providerOptions = mutableListOf() - if (searchable is LauncherApp) { + if (searchable is Application) { val iconPackIcons = iconPackManager.getAllIconPackIcons( - searchable.launcherActivityInfo.componentName + searchable.componentName ) providerOptions.addAll( diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CalendarIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CalendarIconProvider.kt index 93d1e623..a900dfd9 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CalendarIconProvider.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CalendarIconProvider.kt @@ -6,15 +6,15 @@ import android.content.pm.PackageManager import de.mm20.launcher2.icons.DynamicCalendarIcon import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.ktx.obtainTypedArrayOrNull +import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.LauncherApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class CalendarIconProvider(val context: Context, val themed: Boolean): IconProvider { override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? = withContext(Dispatchers.IO) { - if(searchable !is LauncherApp) return@withContext null - val component = ComponentName(searchable.`package`, searchable.activity) + if(searchable !is Application) return@withContext null + val component = searchable.componentName val pm = context.packageManager val ai = try { pm.getActivityInfo(component, PackageManager.GET_META_DATA) diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CompatIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CompatIconProvider.kt index 81d7c799..73b1bbe9 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CompatIconProvider.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CompatIconProvider.kt @@ -6,8 +6,8 @@ import android.content.pm.PackageManager import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.compat.AdaptiveIconDrawableCompat import de.mm20.launcher2.icons.compat.toLauncherIcon +import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.LauncherApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -16,8 +16,8 @@ class CompatIconProvider( private val themed: Boolean = false, ) : IconProvider { override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? { - if (searchable !is LauncherApp) return null - val component = ComponentName(searchable.`package`, searchable.activity) + if (searchable !is Application) return null + val component = searchable.componentName val icon = withContext(Dispatchers.IO) { val activityInfo = try { diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/DynamicClockIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/DynamicClockIconProvider.kt index 172c8bc4..0076223a 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/DynamicClockIconProvider.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/DynamicClockIconProvider.kt @@ -6,18 +6,18 @@ import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.compat.AdaptiveIconDrawableCompat import de.mm20.launcher2.icons.compat.ClockIconConfig import de.mm20.launcher2.icons.compat.toLauncherIcon +import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.LauncherApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class DynamicClockIconProvider(val context: Context, private val themed: Boolean) : IconProvider { override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? = withContext(Dispatchers.IO) { - if (searchable !is LauncherApp) return@withContext null + if (searchable !is Application) return@withContext null val pm = context.packageManager val appInfo = try { pm.getApplicationInfo( - searchable.`package`, + searchable.componentName.packageName, PackageManager.GET_META_DATA ) } catch (e: PackageManager.NameNotFoundException) { @@ -47,7 +47,7 @@ class DynamicClockIconProvider(val context: Context, private val themed: Boolean // Workaround for Google Clock themed icon because it is weird and I don't understand // how to get the correct layers from the drawable without hardcoding them here. - val clockConfig = if (themed && searchable.`package` == "com.google.android.deskclock") { + val clockConfig = if (themed && searchable.componentName.packageName == "com.google.android.deskclock") { ClockIconConfig( hourLayer = 0, minuteLayer = 2, diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/IconPackIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/IconPackIconProvider.kt index e0fe07e0..0d18b074 100644 --- a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/IconPackIconProvider.kt +++ b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/IconPackIconProvider.kt @@ -1,10 +1,12 @@ package de.mm20.launcher2.icons.providers -import android.content.ComponentName import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import androidx.core.content.getSystemService import de.mm20.launcher2.icons.* +import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.SavableSearchable -import de.mm20.launcher2.search.data.LauncherApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -15,15 +17,20 @@ class IconPackIconProvider( private val allowThemed: Boolean, ): IconProvider { override suspend fun getIcon(searchable: SavableSearchable, size: Int): LauncherIcon? { - if (searchable !is LauncherApp) return null + if (searchable !is Application) return null - return iconPackManager.getIcon(iconPack.packageName, searchable.`package`, searchable.activity, allowThemed) + + return iconPackManager.getIcon(iconPack.packageName, searchable.componentName.packageName, searchable.componentName.className, allowThemed) ?: iconPackManager.generateIcon( context, iconPack.packageName, baseIcon = withContext(Dispatchers.IO) { - searchable.launcherActivityInfo.getIcon(context.resources.displayMetrics.densityDpi) - }, + val ai = context.getSystemService()?.resolveActivity( + Intent().setComponent(searchable.componentName), + searchable.user + ) + ai?.getIcon(context.resources.displayMetrics.densityDpi) + } ?: return null, size = size, ) } diff --git a/services/search/build.gradle.kts b/services/search/build.gradle.kts index 591be120..fbb97dc5 100644 --- a/services/search/build.gradle.kts +++ b/services/search/build.gradle.kts @@ -46,20 +46,12 @@ dependencies { implementation(libs.okhttp) implementation(libs.coil.core) - implementation(project(":data:applications")) - implementation(project(":data:appshortcuts")) implementation(project(":data:calculator")) - implementation(project(":data:calendar")) - implementation(project(":data:contacts")) - implementation(project(":data:files")) implementation(project(":data:unitconverter")) - implementation(project(":data:websites")) - implementation(project(":data:wikipedia")) implementation(project(":data:customattrs")) implementation(project(":data:search-actions")) implementation(project(":core:base")) - implementation(project(":core:database")) implementation(project(":core:preferences")) implementation(project(":core:crashreporter")) implementation(project(":core:ktx")) diff --git a/services/search/src/main/java/de/mm20/launcher2/search/Module.kt b/services/search/src/main/java/de/mm20/launcher2/search/Module.kt index 8ee8a1a2..c85889c8 100644 --- a/services/search/src/main/java/de/mm20/launcher2/search/Module.kt +++ b/services/search/src/main/java/de/mm20/launcher2/search/Module.kt @@ -1,20 +1,20 @@ package de.mm20.launcher2.search -import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named import org.koin.dsl.module val searchModule = module { single { SearchServiceImpl( + get(named()), + get(named()), + get(named()), + get(named()), + get(named()), get(), get(), get(), - get(), - get(), - get(), - get(), - get(), - get(), + get(named()), get(), get(), ) diff --git a/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt b/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt index bb31f870..cbe83a27 100644 --- a/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt +++ b/services/search/src/main/java/de/mm20/launcher2/search/SearchService.kt @@ -1,13 +1,8 @@ package de.mm20.launcher2.search -import de.mm20.launcher2.applications.AppRepository -import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.calculator.CalculatorRepository -import de.mm20.launcher2.calendar.CalendarRepository -import de.mm20.launcher2.contacts.ContactRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.utils.withCustomLabels -import de.mm20.launcher2.files.FileRepository import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings.AppShortcutSearchSettings import de.mm20.launcher2.preferences.Settings.CalculatorSearchSettings @@ -17,25 +12,11 @@ import de.mm20.launcher2.preferences.Settings.FilesSearchSettings import de.mm20.launcher2.preferences.Settings.UnitConverterSearchSettings import de.mm20.launcher2.preferences.Settings.WebsiteSearchSettings import de.mm20.launcher2.preferences.Settings.WikipediaSearchSettings -import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.Calculator -import de.mm20.launcher2.search.data.CalendarEvent -import de.mm20.launcher2.search.data.Contact -import de.mm20.launcher2.search.data.File -import de.mm20.launcher2.search.data.GDriveFile -import de.mm20.launcher2.search.data.LauncherApp -import de.mm20.launcher2.search.data.LocalFile -import de.mm20.launcher2.search.data.NextcloudFile -import de.mm20.launcher2.search.data.OneDriveFile -import de.mm20.launcher2.search.data.OwncloudFile 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.SearchActionService import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.unitconverter.UnitConverterRepository -import de.mm20.launcher2.websites.WebsiteRepository -import de.mm20.launcher2.wikipedia.WikipediaRepository import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -84,15 +65,15 @@ interface SearchService { } internal class SearchServiceImpl( - private val appRepository: AppRepository, - private val appShortcutRepository: AppShortcutRepository, - private val calendarRepository: CalendarRepository, - private val contactRepository: ContactRepository, - private val fileRepository: FileRepository, - private val wikipediaRepository: WikipediaRepository, + private val appRepository: SearchableRepository, + private val appShortcutRepository: SearchableRepository, + private val calendarRepository: SearchableRepository, + private val contactRepository: SearchableRepository, + private val fileRepository: SearchableRepository, + private val wikipediaRepository: SearchableRepository
, private val unitConverterRepository: UnitConverterRepository, private val calculatorRepository: CalculatorRepository, - private val websiteRepository: WebsiteRepository, + private val websiteRepository: SearchableRepository, private val searchActionService: SearchActionService, private val customAttributesRepository: CustomAttributesRepository, ) : SearchService { @@ -184,7 +165,6 @@ internal class SearchServiceImpl( if (websites.enabled) { launch { websiteRepository.search(query) - .map { it?.let { listOf(it) } ?: listOf() } .withCustomLabels(customAttributesRepository) .collectLatest { r -> results.update { @@ -196,8 +176,7 @@ internal class SearchServiceImpl( if (wikipedia.enabled) { launch { delay(750) - wikipediaRepository.search(query, loadImages = wikipedia.images) - .map { it?.let { listOf(it) } ?: listOf() } + wikipediaRepository.search(query) .withCustomLabels(customAttributesRepository) .collectLatest { r -> results.update { @@ -210,11 +189,6 @@ internal class SearchServiceImpl( launch { fileRepository.search( query, - local = files.localFiles, - nextcloud = files.nextcloud, - owncloud = files.owncloud, - onedrive = files.onedrive, - gdrive = files.gdrive, ) .withCustomLabels(customAttributesRepository) .collectLatest { r -> @@ -229,22 +203,7 @@ internal class SearchServiceImpl( .withCustomLabels(customAttributesRepository) .collectLatest { r -> results.update { - it.copy( - other = r - .filter { - it is LauncherApp || - shortcuts.enabled && it is AppShortcut || - files.localFiles && it is LocalFile || - files.nextcloud && it is NextcloudFile || - files.owncloud && it is OwncloudFile || - files.onedrive && it is OneDriveFile || - files.gdrive && it is GDriveFile || - wikipedia.enabled && it is Wikipedia || - websites.enabled && it is Website || - calendars.enabled && it is CalendarEvent || - contacts.enabled && it is Contact - }.toImmutableList() - ) + it.copy(other = r.toImmutableList()) } } } @@ -257,7 +216,7 @@ internal class SearchServiceImpl( } data class SearchResults( - val apps: ImmutableList? = null, + val apps: ImmutableList? = null, val shortcuts: ImmutableList? = null, val contacts: ImmutableList? = null, val calendars: ImmutableList? = null, @@ -265,7 +224,7 @@ data class SearchResults( val calculators: ImmutableList? = null, val unitConverters: ImmutableList? = null, val websites: ImmutableList? = null, - val wikipedia: ImmutableList? = null, + val wikipedia: ImmutableList
? = null, val searchActions: ImmutableList? = null, val other: ImmutableList? = null, ) diff --git a/services/search/src/main/java/de/mm20/launcher2/search/data/Websearch.kt b/services/search/src/main/java/de/mm20/launcher2/search/data/Websearch.kt deleted file mode 100644 index 5e44d9de..00000000 --- a/services/search/src/main/java/de/mm20/launcher2/search/data/Websearch.kt +++ /dev/null @@ -1,81 +0,0 @@ -package de.mm20.launcher2.search.data - -import android.content.Intent -import android.net.Uri -import de.mm20.launcher2.database.entities.WebsearchEntity -import java.net.URLEncoder -import java.nio.charset.Charset -import java.nio.charset.StandardCharsets - -class Websearch( - var urlTemplate: String, - var label: String, - var color: Int, - var icon: String?, - var id: Long? = null, - var encoding: QueryEncoding = QueryEncoding.UrlEncode, - val query: String? = null, -) { - - constructor(entity: WebsearchEntity, query: String? = null) : this( - urlTemplate = entity.urlTemplate, - label = entity.label, - icon = entity.icon, - color = entity.color, - id = entity.id, - query = query, - encoding = QueryEncoding.fromInt(entity.encoding) - ) - - fun toDatabaseEntity(): WebsearchEntity { - return WebsearchEntity( - urlTemplate = urlTemplate, - color = color, - icon = icon, - label = label, - id = id, - encoding = encoding.toInt() - ) - } - - fun getLaunchIntent(): Intent? { - if (query == null) return null - val intent = Intent(Intent.ACTION_VIEW) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - val url = urlTemplate.replace("\${1}", encodeQuery(query, encoding)) - intent.data = Uri.parse(url) - return intent - } - - private fun encodeQuery(query: String, encoding: QueryEncoding): String { - return when(encoding) { - QueryEncoding.UrlEncode -> Uri.encode(query) - QueryEncoding.FormData -> URLEncoder.encode(query, "UTF-8") - QueryEncoding.None -> query - } - } - - enum class QueryEncoding { - UrlEncode, - FormData, - None; - - fun toInt(): Int { - return when (this) { - UrlEncode -> 0 - FormData -> 1 - None -> 2 - } - } - - companion object { - fun fromInt(value: Int?): QueryEncoding { - return when (value) { - 1 -> FormData - 2 -> None - else -> UrlEncode - } - } - } - } -} \ 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 88ba2c2a..fd0822dd 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.searchable.SearchableRepository +import de.mm20.launcher2.searchable.SavableSearchableRepository 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 searchableRepository: SearchableRepository, + private val searchableRepository: SavableSearchableRepository, ) : TagsService { private val scope = CoroutineScope(Job() + Dispatchers.Default) override fun getAllTags(startsWith: String?): Flow> {