Annual refactor

This commit is contained in:
MM20 2023-10-28 14:14:08 +02:00
parent 4f64652aae
commit 957358c79a
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
110 changed files with 1786 additions and 1663 deletions

View File

@ -3,8 +3,7 @@ package de.mm20.launcher2.activity
import android.app.Activity import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import de.mm20.launcher2.searchable.SearchableRepository import de.mm20.launcher2.appshortcuts.AppShortcut
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -14,7 +13,7 @@ class AddItemActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val shortcut = AppShortcut.fromPinRequestIntent(this, intent) val shortcut = AppShortcut(this, intent)
if (shortcut != null) { if (shortcut != null) {
favoritesService.pinItem(shortcut) favoritesService.pinItem(shortcut)
} else { } else {

View File

@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.data.customattrs.utils.withCustomLabels import de.mm20.launcher2.data.customattrs.utils.withCustomLabels
import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag

View File

@ -31,8 +31,8 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.compose.AsyncImage import coil.compose.AsyncImage
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.abs import kotlin.math.abs
@ -107,23 +107,12 @@ fun rememberSplashScreenData(searchable: SavableSearchable?): SplashScreenData {
LaunchedEffect(searchable) { LaunchedEffect(searchable) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (searchable is LauncherApp) { if (searchable is Application) {
val activityInfo = if (isAtLeastApiLevel(31)) { val activityInfo = searchable.getActivityInfo(context) ?: return@withContext
searchable.launcherActivityInfo.activityInfo
} else {
try {
context.packageManager.getActivityInfo(
searchable.launcherActivityInfo.componentName,
0
)
} catch (e: PackageManager.NameNotFoundException) {
null
}
} ?: return@withContext
val themeRes = activityInfo.themeResource val themeRes = activityInfo.themeResource
val ctx = try { val ctx = try {
context.createPackageContext( context.createPackageContext(
searchable.`package`, searchable.componentName.packageName,
Context.CONTEXT_IGNORE_SECURITY Context.CONTEXT_IGNORE_SECURITY
) )
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {

View File

@ -8,7 +8,7 @@ import androidx.compose.runtime.setValue
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.globalactions.GlobalActionsService
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
@ -35,7 +35,7 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
private val globalActionsService: GlobalActionsService by inject() private val globalActionsService: GlobalActionsService by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val searchableRepository: SearchableRepository by inject() private val searchableRepository: SavableSearchableRepository by inject()
private var isSystemInDarkMode = MutableStateFlow(false) private var isSystemInDarkMode = MutableStateFlow(false)

View File

@ -1,10 +1,7 @@
package de.mm20.launcher2.ui.launcher.search package de.mm20.launcher2.ui.launcher.search
import androidx.appcompat.app.AppCompatActivity 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.Box
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons 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.Person
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.Tag import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material.icons.rounded.Work import androidx.compose.material.icons.rounded.Work
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalContext 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.favorites.SearchFavoritesVM
import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterItem 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.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.HiddenItemsSheet
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.locals.LocalCardStyle import de.mm20.launcher2.ui.locals.LocalCardStyle
@ -99,7 +89,7 @@ fun SearchColumn(
val events by viewModel.calendarResults val events by viewModel.calendarResults
val unitConverter by viewModel.unitConverterResults val unitConverter by viewModel.unitConverterResults
val calculator by viewModel.calculatorResults val calculator by viewModel.calculatorResults
val wikipedia by viewModel.wikipediaResults val wikipedia by viewModel.articleResults
val website by viewModel.websiteResults val website by viewModel.websiteResults
val hiddenResults by viewModel.hiddenResults val hiddenResults by viewModel.hiddenResults
@ -307,7 +297,7 @@ fun SearchColumn(
) )
for (wiki in wikipedia) { for (wiki in wikipedia) {
SingleResult(highlight = bestMatch == wiki) { SingleResult(highlight = bestMatch == wiki) {
WikipediaItem(wikipedia = wiki) ArticleItem(article = wiki)
} }
} }
for (ws in website) { for (ws in website) {

View File

@ -5,23 +5,24 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.searchable.SearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.Ordering 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.SavableSearchable
import de.mm20.launcher2.search.SearchService import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.Searchable 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.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.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.searchactions.actions.SearchAction
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@ -43,7 +44,7 @@ import org.koin.core.component.inject
class SearchVM : ViewModel(), KoinComponent { class SearchVM : ViewModel(), KoinComponent {
private val favoritesService: FavoritesService by inject() 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 permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
@ -55,13 +56,13 @@ class SearchVM : ViewModel(), KoinComponent {
val searchQuery = mutableStateOf("") val searchQuery = mutableStateOf("")
val isSearchEmpty = mutableStateOf(true) val isSearchEmpty = mutableStateOf(true)
val appResults = mutableStateOf<List<LauncherApp>>(emptyList()) val appResults = mutableStateOf<List<Application>>(emptyList())
val workAppResults = mutableStateOf<List<LauncherApp>>(emptyList()) val workAppResults = mutableStateOf<List<Application>>(emptyList())
val appShortcutResults = mutableStateOf<List<AppShortcut>>(emptyList()) val appShortcutResults = mutableStateOf<List<AppShortcut>>(emptyList())
val fileResults = mutableStateOf<List<File>>(emptyList()) val fileResults = mutableStateOf<List<File>>(emptyList())
val contactResults = mutableStateOf<List<Contact>>(emptyList()) val contactResults = mutableStateOf<List<Contact>>(emptyList())
val calendarResults = mutableStateOf<List<CalendarEvent>>(emptyList()) val calendarResults = mutableStateOf<List<CalendarEvent>>(emptyList())
val wikipediaResults = mutableStateOf<List<Wikipedia>>(emptyList()) val articleResults = mutableStateOf<List<Article>>(emptyList())
val websiteResults = mutableStateOf<List<Website>>(emptyList()) val websiteResults = mutableStateOf<List<Website>>(emptyList())
val calculatorResults = mutableStateOf<List<Calculator>>(emptyList()) val calculatorResults = mutableStateOf<List<Calculator>>(emptyList())
val unitConverterResults = mutableStateOf<List<UnitConverter>>(emptyList()) val unitConverterResults = mutableStateOf<List<UnitConverter>>(emptyList())
@ -185,15 +186,15 @@ class SearchVM : ViewModel(), KoinComponent {
hiddenItemKeys.collectLatest { hiddenKeys -> hiddenItemKeys.collectLatest { hiddenKeys ->
val hidden = mutableListOf<SavableSearchable>() val hidden = mutableListOf<SavableSearchable>()
val apps = mutableListOf<LauncherApp>() val apps = mutableListOf<Application>()
val workApps = mutableListOf<LauncherApp>() val workApps = mutableListOf<Application>()
val shortcuts = mutableListOf<AppShortcut>() val shortcuts = mutableListOf<AppShortcut>()
val files = mutableListOf<File>() val files = mutableListOf<File>()
val contacts = mutableListOf<Contact>() val contacts = mutableListOf<Contact>()
val events = mutableListOf<CalendarEvent>() val events = mutableListOf<CalendarEvent>()
val unitConv = mutableListOf<UnitConverter>() val unitConv = mutableListOf<UnitConverter>()
val calc = mutableListOf<Calculator>() val calc = mutableListOf<Calculator>()
val wikipedia = mutableListOf<Wikipedia>() val articles = mutableListOf<Article>()
val website = mutableListOf<Website>() val website = mutableListOf<Website>()
val actions = mutableListOf<SearchAction>() val actions = mutableListOf<SearchAction>()
for (r in resultsList) { for (r in resultsList) {
@ -202,8 +203,8 @@ class SearchVM : ViewModel(), KoinComponent {
hidden.add(r) hidden.add(r)
} }
r is LauncherApp && !r.isMainProfile -> workApps.add(r) r is Application && r.profile == AppProfile.Work -> workApps.add(r)
r is LauncherApp -> apps.add(r) r is Application -> apps.add(r)
r is AppShortcut -> shortcuts.add(r) r is AppShortcut -> shortcuts.add(r)
r is File -> files.add(r) r is File -> files.add(r)
r is Contact -> contacts.add(r) r is Contact -> contacts.add(r)
@ -211,7 +212,7 @@ class SearchVM : ViewModel(), KoinComponent {
r is UnitConverter -> unitConv.add(r) r is UnitConverter -> unitConv.add(r)
r is Calculator -> calc.add(r) r is Calculator -> calc.add(r)
r is Website -> website.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) r is SearchAction -> actions.add(r)
} }
} }
@ -225,7 +226,7 @@ class SearchVM : ViewModel(), KoinComponent {
calc, calc,
events, events,
contacts, contacts,
wikipedia, articles,
website, website,
files, files,
actions actions
@ -238,7 +239,7 @@ class SearchVM : ViewModel(), KoinComponent {
fileResults.value = files fileResults.value = files
contactResults.value = contacts contactResults.value = contacts
calendarResults.value = events calendarResults.value = events
wikipediaResults.value = wikipedia articleResults.value = articles
websiteResults.value = website websiteResults.value = website
calculatorResults.value = calc calculatorResults.value = calc
unitConverterResults.value = unitConv unitConverterResults.value = unitConv

View File

@ -66,7 +66,7 @@ import androidx.lifecycle.lifecycleScope
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import de.mm20.launcher2.crashreporter.CrashReporter 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.R
import de.mm20.launcher2.ui.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.DefaultToolbarAction
import de.mm20.launcher2.ui.component.ShapedLauncherIcon 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.locals.LocalSnackbarHostState
import de.mm20.launcher2.ui.modifier.scale import de.mm20.launcher2.ui.modifier.scale
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.pow
@Composable @Composable
fun AppItem( fun AppItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
app: LauncherApp, app: Application,
onBack: () -> Unit onBack: () -> Unit
) { ) {
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${app.key}") val viewModel: SearchableItemVM = listItemViewModel(key = "search-${app.key}")
@ -127,7 +126,7 @@ fun AppItem(
} }
app.version?.let { app.versionName?.let {
Text( Text(
text = stringResource(R.string.app_info_version, it), text = stringResource(R.string.app_info_version, it),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@ -137,7 +136,7 @@ fun AppItem(
) )
} }
Text( Text(
text = app.`package`, text = app.componentName.packageName,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 1.dp), modifier = Modifier.padding(top = 1.dp),
maxLines = 1, maxLines = 1,
@ -210,20 +209,21 @@ fun AppItem(
for (shortcut in shortcuts) { for (shortcut in shortcuts) {
val title = val title =
shortcut.launcherShortcut.shortLabel shortcut.labelOverride ?: shortcut.label
?: shortcut.launcherShortcut.longLabel
?: continue
val isPinned by remember(shortcut) { viewModel.isShortcutPinned(shortcut) }.collectAsState( val isPinned by remember(shortcut) { viewModel.isShortcutPinned(shortcut) }.collectAsState(
false false
) )
val icon = val iconSizePx = InputChipDefaults.AvatarSize.toPixels()
val icon by
remember { remember {
viewModel.getShortcutIcon( viewModel.getShortcutIcon(
context, context,
shortcut.launcherShortcut shortcut,
iconSizePx.toInt()
) )
} }.collectAsState(null)
InputChip( InputChip(
modifier = Modifier.width(IntrinsicSize.Max), modifier = Modifier.width(IntrinsicSize.Max),
@ -233,19 +233,17 @@ fun AppItem(
}, },
label = { label = {
Text( Text(
title.toString(), title,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
}, },
avatar = { avatar = {
AsyncImage( ShapedLauncherIcon(
model = icon, size = InputChipDefaults.AvatarSize,
contentDescription = null, icon = { icon },
modifier = Modifier shape = CircleShape,
.clip(CircleShape)
.size(InputChipDefaults.AvatarSize),
) )
}, },
trailingIcon = if (LocalFavoritesEnabled.current) { trailingIcon = if (LocalFavoritesEnabled.current) {
@ -311,7 +309,7 @@ fun AppItem(
label = stringResource(R.string.menu_app_info), label = stringResource(R.string.menu_app_info),
icon = Icons.Rounded.Info icon = Icons.Rounded.Info
) { ) {
app.openAppInfo(context) app.openAppDetails(context)
}) })
toolbarActions.add( toolbarActions.add(
@ -429,7 +427,7 @@ fun AppItem(
@Composable @Composable
fun AppItemGridPopup( fun AppItemGridPopup(
app: LauncherApp, app: Application,
show: MutableTransitionState<Boolean>, show: MutableTransitionState<Boolean>,
animationProgress: Float, animationProgress: Float,
origin: Rect, origin: Rect,
@ -454,7 +452,7 @@ fun AppItemGridPopup(
transformOrigin = TransformOrigin(1f, 0f) transformOrigin = TransformOrigin(1f, 0f)
) )
.offset( .offset(
x = lerp(16.dp, 0.dp, animationProgress), x = lerp(16.dp, 0.dp, animationProgress),
y = lerp(-16.dp, 0.dp, animationProgress) y = lerp(-16.dp, 0.dp, animationProgress)
), ),
app = app, app = app,

View File

@ -41,13 +41,14 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.roundToIntRect
import androidx.lifecycle.lifecycleScope 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.R
import de.mm20.launcher2.ui.animation.animateTextStyleAsState import de.mm20.launcher2.ui.animation.animateTextStyleAsState
import de.mm20.launcher2.ui.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.DefaultToolbarAction
@ -83,12 +84,13 @@ fun CalendarItem(
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
val darkMode = LocalDarkTheme.current val darkMode = LocalDarkTheme.current
val secondaryColor = MaterialTheme.colorScheme.secondary
Row( Row(
modifier = modifier modifier = modifier
.drawBehind { .drawBehind {
val color = TonalPalette val color = TonalPalette
.fromInt(calendar.color) .fromInt(calendar.color ?: secondaryColor.toArgb())
.tone( .tone(
if (darkMode) 80 else 40 if (darkMode) 80 else 40
) )
@ -151,7 +153,7 @@ fun CalendarItem(
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
if (calendar.description.isNotBlank()) { if (calendar.description != null) {
Row( Row(
Modifier Modifier
.fillMaxWidth(), .fillMaxWidth(),
@ -163,7 +165,7 @@ fun CalendarItem(
contentDescription = null contentDescription = null
) )
Text( Text(
text = calendar.description, text = calendar.description!!,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
@ -185,7 +187,7 @@ fun CalendarItem(
) )
} }
} }
if (calendar.location.isNotBlank()) { if (calendar.location != null) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
@ -200,7 +202,7 @@ fun CalendarItem(
contentDescription = null contentDescription = null
) )
Text( Text(
text = calendar.location, text = calendar.location!!,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
@ -320,8 +322,7 @@ fun CalendarItemGridPopup(
) { ) {
CalendarItem( CalendarItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth(),
.background(Color(calendar.color).copy(alpha = 1f - animationProgress)),
calendar = calendar, calendar = calendar,
showDetails = true, showDetails = true,
onBack = onDismiss onBack = onDismiss

View File

@ -4,27 +4,22 @@ import android.content.Context
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo import android.content.pm.ShortcutInfo
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.service.notification.StatusBarNotification
import android.util.Log
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.badges.BadgeService import de.mm20.launcher2.badges.BadgeService
import de.mm20.launcher2.files.FileRepository
import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.notifications.Notification import de.mm20.launcher2.notifications.Notification
import de.mm20.launcher2.notifications.NotificationRepository import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.services.tags.TagsService import de.mm20.launcher2.services.tags.TagsService
import de.mm20.launcher2.ui.launcher.search.ListItemViewModel 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.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -46,7 +43,6 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
private val tagsService: TagsService by inject() private val tagsService: TagsService by inject()
private val notificationRepository: NotificationRepository by inject() private val notificationRepository: NotificationRepository by inject()
private val appShortcutRepository: AppShortcutRepository by inject() private val appShortcutRepository: AppShortcutRepository by inject()
private val fileRepository: FileRepository by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val searchable = MutableStateFlow<SavableSearchable?>(null) private val searchable = MutableStateFlow<SavableSearchable?>(null)
@ -93,13 +89,19 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
} }
val notifications = searchable.flatMapLatest { searchable -> val notifications = searchable.flatMapLatest { searchable ->
if (searchable !is LauncherApp) emptyFlow() if (searchable !is Application) emptyFlow()
else notificationRepository.notifications.map { it.filter { it.packageName == searchable.`package` && !it.isGroupSummary } } else notificationRepository.notifications.map { it.filter { it.packageName == searchable.componentName.packageName && !it.isGroupSummary } }
} }
val shortcuts = searchable.map { val shortcuts = searchable.map {
if (it !is LauncherApp) emptyList() if (it !is Application) emptyList()
else appShortcutRepository.getShortcutsForActivity(it.launcherActivityInfo, 5) else appShortcutRepository
.findMany(
componentName = it.componentName,
user = it.user,
manifest = true,
dynamic = true,
).first()
} }
open fun launch(context: Context, bounds: Rect? = null): Boolean { open fun launch(context: Context, bounds: Rect? = null): Boolean {
@ -120,7 +122,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
if (searchable.launch(context, bundle)) { if (searchable.launch(context, bundle)) {
favoritesService.reportLaunch(searchable) favoritesService.reportLaunch(searchable)
return true return true
} else if (searchable is LauncherApp || searchable is AppShortcut) { } else if (searchable is Application || searchable is AppShortcut) {
favoritesService.reset(searchable) favoritesService.reset(searchable)
} }
return false return false
@ -130,9 +132,8 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
notificationRepository.cancelNotification(notification) notificationRepository.cancelNotification(notification)
} }
fun getShortcutIcon(context: Context, shortcut: ShortcutInfo): Drawable? { fun getShortcutIcon(context: Context, shortcut: AppShortcut, size: Int): Flow<LauncherIcon?> {
val launcherApps = context.getSystemService<LauncherApps>() ?: return null return iconService.getIcon(shortcut, size)
return launcherApps.getShortcutIconDrawable(shortcut, 0)
} }
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> { fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
@ -151,10 +152,18 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
shortcut.launch(context, null) shortcut.launch(context, null)
} }
fun delete() { fun delete(context: Context) {
val searchable = searchable.value ?: return val searchable = searchable.value ?: return
if (searchable is File) fileRepository.deleteFile(searchable) if (searchable is File) {
if (searchable is LauncherShortcut) appShortcutRepository.removePinnedShortcut(searchable) viewModelScope.launch {
searchable.delete(context.applicationContext)
}
}
if (searchable is AppShortcut) {
viewModelScope.launch {
searchable.delete(context.applicationContext)
}
}
favoritesService.reset(searchable) favoritesService.reset(searchable)
} }

View File

@ -1,7 +1,5 @@
package de.mm20.launcher2.ui.launcher.search.common.grid package de.mm20.launcher2.ui.launcher.search.common.grid
import android.content.ComponentName
import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.MutableTransitionState 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.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.SavableSearchable
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.data.Contact import de.mm20.launcher2.search.Article
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.Website
import de.mm20.launcher2.search.data.Website
import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.ui.component.LauncherCard import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.component.LocalIconShape import de.mm20.launcher2.ui.component.LocalIconShape
import de.mm20.launcher2.ui.component.ShapedLauncherIcon 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.listItemViewModel
import de.mm20.launcher2.ui.launcher.search.shortcut.ShortcutItemGridPopup 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.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.EnterHomeTransitionParams
import de.mm20.launcher2.ui.launcher.transitions.HandleEnterHomeTransition import de.mm20.launcher2.ui.launcher.transitions.HandleEnterHomeTransition
import de.mm20.launcher2.ui.locals.LocalGridSettings import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.ui.locals.LocalWindowSize import de.mm20.launcher2.ui.locals.LocalWindowSize
import de.mm20.launcher2.ui.overlays.Overlay import de.mm20.launcher2.ui.overlays.Overlay
import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
@ -121,9 +117,9 @@ fun GridItem(
val windowSize = LocalWindowSize.current val windowSize = LocalWindowSize.current
if (item is LauncherApp) { if (item is Application) {
HandleEnterHomeTransition { HandleEnterHomeTransition {
val cn = ComponentName(item.`package`, item.activity) val cn = item.componentName
if ( if (
it.componentName == cn && it.componentName == cn &&
bounds.right > 0f && bounds.left < windowSize.width && bounds.right > 0f && bounds.left < windowSize.width &&
@ -240,7 +236,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
) )
) { ) {
when (searchable) { when (searchable) {
is LauncherApp -> { is Application -> {
AppItemGridPopup( AppItemGridPopup(
app = searchable, app = searchable,
show = show, show = show,
@ -264,9 +260,9 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
) )
} }
is Wikipedia -> { is Article -> {
WikipediaItemGridPopup( ArticleItemGridPopup(
wikipedia = searchable, article = searchable,
show = show, show = show,
animationProgress = animationProgress.value, animationProgress = animationProgress.value,
origin = origin, origin = origin,

View File

@ -9,9 +9,11 @@ import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp 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.SavableSearchable
import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.ui.component.InnerCard import de.mm20.launcher2.ui.component.InnerCard
import de.mm20.launcher2.ui.ktx.toPixels import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem

View File

@ -17,16 +17,20 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons 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.ArrowBack
import androidx.compose.material.icons.rounded.Call import androidx.compose.material.icons.rounded.Call
import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Email 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.OpenInNew
import androidx.compose.material.icons.rounded.Place import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material.icons.rounded.Visibility import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material.icons.rounded.Whatsapp
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
@ -36,6 +40,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
@ -49,7 +54,8 @@ import androidx.compose.ui.unit.roundToIntRect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import de.mm20.launcher2.ktx.tryStartActivity 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.R
import de.mm20.launcher2.ui.animation.animateTextStyleAsState import de.mm20.launcher2.ui.animation.animateTextStyleAsState
import de.mm20.launcher2.ui.component.Chip import de.mm20.launcher2.ui.component.Chip
@ -144,164 +150,38 @@ fun ContactItem(
} }
AnimatedVisibility(showDetails) { AnimatedVisibility(showDetails) {
val groups = remember {
contact.contactInfos.groupBy { it.type }
}
Column { Column {
if (contact.phones.isNotEmpty()) { for ((type, items) in groups) {
Row( Row(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically 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( LazyRow(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(start = 16.dp), .padding(start = 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
items(contact.phones.toList()) { items(items.toList()) {
Chip( Chip(
modifier = Modifier.padding(end = 16.dp), modifier = Modifier.padding(end = 16.dp),
text = it.label, text = it.label,
onClick = { onClick = {
context.tryStartActivity( context.tryStartActivity(it.intent)
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))
)
} }
) )
} }

View File

@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.roundToIntRect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope 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.R
import de.mm20.launcher2.ui.animation.animateTextStyleAsState import de.mm20.launcher2.ui.animation.animateTextStyleAsState
import de.mm20.launcher2.ui.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.DefaultToolbarAction
@ -236,7 +236,7 @@ fun FileItem(
onDismissRequest = { showConfirmDialog = false }, onDismissRequest = { showConfirmDialog = false },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
viewModel.delete() viewModel.delete(context)
showConfirmDialog = false showConfirmDialog = false
}) { }) {
Text(stringResource(android.R.string.ok)) Text(stringResource(android.R.string.ok))

View File

@ -53,10 +53,7 @@ import androidx.compose.ui.unit.roundToIntRect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.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.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.animation.animateTextStyleAsState import de.mm20.launcher2.ui.animation.animateTextStyleAsState
import de.mm20.launcher2.ui.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.DefaultToolbarAction
@ -101,7 +98,7 @@ fun AppShortcutItem(
Column( Column(
modifier = modifier modifier = modifier
) { ) {
AnimatedVisibility(showDetails && shortcut is UnavailableShortcut) { AnimatedVisibility(showDetails && shortcut.isUnavailable) {
MissingPermissionBanner( MissingPermissionBanner(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp), modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
text = stringResource(R.string.shortcut_unavailable_description, stringResource(R.string.app_name)), text = stringResource(R.string.shortcut_unavailable_description, stringResource(R.string.app_name)),
@ -213,7 +210,7 @@ fun AppShortcutItem(
action = { sheetManager.showCustomizeSearchableModal(shortcut) } action = { sheetManager.showCustomizeSearchableModal(shortcut) }
)) ))
if (shortcut is LauncherShortcut && shortcut.launcherShortcut.isPinned) { if (shortcut.canDelete) {
toolbarActions.add(DefaultToolbarAction( toolbarActions.add(DefaultToolbarAction(
label = stringResource(R.string.menu_delete), label = stringResource(R.string.menu_delete),
icon = Icons.Rounded.Delete, icon = Icons.Rounded.Delete,
@ -276,7 +273,7 @@ fun AppShortcutItem(
text = { Text(stringResource(R.string.alert_delete_shortcut, shortcut.label)) }, text = { Text(stringResource(R.string.alert_delete_shortcut, shortcut.label)) },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
viewModel.delete() viewModel.delete(context)
requestDelete = false requestDelete = false
}) { }) {
Text(stringResource(android.R.string.ok)) 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
}

View File

@ -32,7 +32,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.roundToIntRect
import coil.compose.AsyncImage 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.R
import de.mm20.launcher2.ui.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.DefaultToolbarAction
import de.mm20.launcher2.ui.component.Toolbar import de.mm20.launcher2.ui.component.Toolbar
@ -64,13 +64,13 @@ fun WebsiteItem(
viewModel.launch(context) viewModel.launch(context)
} }
) { ) {
if (website.image.isNotBlank()) { if (website.imageUrl != null) {
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(16f / 9f) .aspectRatio(16f / 9f)
.background(MaterialTheme.colorScheme.secondaryContainer), .background(MaterialTheme.colorScheme.secondaryContainer),
model = website.image, model = website.imageUrl,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
contentDescription = null contentDescription = null
) )
@ -93,7 +93,7 @@ fun WebsiteItem(
} }
Text( Text(
modifier = Modifier.padding(vertical = 8.dp), modifier = Modifier.padding(vertical = 8.dp),
text = website.description, text = website.description ?: "",
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }

View File

@ -32,7 +32,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.roundToIntRect
import coil.compose.AsyncImage 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.R
import de.mm20.launcher2.ui.component.DefaultToolbarAction import de.mm20.launcher2.ui.component.DefaultToolbarAction
import de.mm20.launcher2.ui.component.Toolbar 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 import de.mm20.launcher2.ui.utils.htmlToAnnotatedString
@Composable @Composable
fun WikipediaItem( fun ArticleItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
wikipedia: Wikipedia, article: Article,
onBack: (() -> Unit)? = null onBack: (() -> Unit)? = null
) { ) {
val context = LocalContext.current 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() val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
LaunchedEffect(wikipedia, iconSize) { LaunchedEffect(article, iconSize) {
viewModel.init(wikipedia, iconSize.toInt()) viewModel.init(article, iconSize.toInt())
} }
Column( Column(
@ -65,13 +65,13 @@ fun WikipediaItem(
viewModel.launch(context) viewModel.launch(context)
} }
) { ) {
if (!wikipedia.image.isNullOrEmpty()) { if (!article.imageUrl.isNullOrEmpty()) {
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(16f / 9f) .aspectRatio(16f / 9f)
.background(MaterialTheme.colorScheme.secondaryContainer), .background(MaterialTheme.colorScheme.secondaryContainer),
model = wikipedia.image, model = article.imageUrl,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
contentDescription = null contentDescription = null
) )
@ -80,7 +80,7 @@ fun WikipediaItem(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
) { ) {
Text( Text(
text = wikipedia.label, text = article.label,
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
val tags by viewModel.tags.collectAsState(emptyList()) val tags by viewModel.tags.collectAsState(emptyList())
@ -100,7 +100,7 @@ fun WikipediaItem(
) )
Text( Text(
modifier = Modifier.padding(vertical = 8.dp), modifier = Modifier.padding(vertical = 8.dp),
text = htmlToAnnotatedString(wikipedia.text), text = htmlToAnnotatedString(article.text),
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
@ -134,7 +134,7 @@ fun WikipediaItem(
label = stringResource(R.string.menu_share), label = stringResource(R.string.menu_share),
icon = Icons.Rounded.Share, icon = Icons.Rounded.Share,
action = { action = {
wikipedia.share(context) article.share(context)
} }
) )
) )
@ -143,7 +143,7 @@ fun WikipediaItem(
toolbarActions.add(DefaultToolbarAction( toolbarActions.add(DefaultToolbarAction(
label = stringResource(R.string.menu_customize), label = stringResource(R.string.menu_customize),
icon = Icons.Rounded.Edit, icon = Icons.Rounded.Edit,
action = { sheetManager.showCustomizeSearchableModal(wikipedia) } action = { sheetManager.showCustomizeSearchableModal(article) }
)) ))
Toolbar( Toolbar(
@ -160,8 +160,8 @@ fun WikipediaItem(
} }
@Composable @Composable
fun WikipediaItemGridPopup( fun ArticleItemGridPopup(
wikipedia: Wikipedia, article: Article,
show: MutableTransitionState<Boolean>, show: MutableTransitionState<Boolean>,
animationProgress: Float, animationProgress: Float,
origin: Rect, origin: Rect,
@ -178,10 +178,10 @@ fun WikipediaItemGridPopup(
shrinkTowards = Alignment.Center, shrinkTowards = Alignment.Center,
) { origin.roundToIntRect().size }, ) { origin.roundToIntRect().size },
) { ) {
WikipediaItem( ArticleItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
wikipedia = wikipedia, article = article,
onBack = onDismiss onBack = onDismiss
) )
} }

View File

@ -74,7 +74,7 @@ import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.UserCalendar import de.mm20.launcher2.calendar.UserCalendar
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage import de.mm20.launcher2.ui.component.LargeMessage

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.launcher.sheets
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.IntentSenderRequest import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts 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.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign 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.toOffset
import androidx.compose.ui.unit.toSize import androidx.compose.ui.unit.toSize
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable 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 val activity = LocalLifecycleOwner.current as AppCompatActivity
LazyColumn( LazyColumn(
contentPadding = paddingValues contentPadding = paddingValues
@ -664,31 +666,28 @@ fun ShortcutPicker(viewModel: EditFavoritesSheetVM, paddingValues: PaddingValues
} }
} }
items(shortcutActivities) { items(shortcutActivities) {
val icon by remember(it.key) { viewModel.getIcon(it, iconSize) }.collectAsState(null) val icon by remember(it) { it.getIcon(context) }.collectAsState(null)
val badge by remember(it.key) { viewModel.getBadge(it) }.collectAsState(null)
OutlinedCard( OutlinedCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 4.dp), .padding(vertical = 4.dp),
onClick = { onClick = {
val launcherApps = val intent = it.getIntent(context) ?: return@OutlinedCard run {
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps Log.e("MM20", "Couldn't get intent for shortcut")
val sender = }
launcherApps.getShortcutConfigActivityIntent(it.launcherActivityInfo) activityLauncher.launch(IntentSenderRequest.Builder(intent).build(), null)
?: return@OutlinedCard
activityLauncher.launch(IntentSenderRequest.Builder(sender).build(), null)
}) { }) {
Row( Row(
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
ShapedLauncherIcon( AsyncImage(
size = 48.dp, model = icon,
icon = { icon }, contentDescription = null,
badge = { badge }, modifier = Modifier.size(48.dp)
) )
Text( Text(
text = it.labelOverride ?: it.label, text = it.label,
modifier = Modifier.padding(start = 16.dp), modifier = Modifier.padding(start = 16.dp),
style = MaterialTheme.typography.titleSmall style = MaterialTheme.typography.titleSmall
) )

View File

@ -19,7 +19,7 @@ import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable 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.Searchable
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
@ -210,7 +210,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
fun createShortcut(context: Context, data: Intent?) { fun createShortcut(context: Context, data: Intent?) {
data ?: return cancelPickShortcut() data ?: return cancelPickShortcut()
val shortcut = AppShortcut.fromPinRequestIntent(context, data) val shortcut = AppShortcut(context, data)
if (shortcut == null) { if (shortcut == null) {
cancelPickShortcut() cancelPickShortcut()

View File

@ -9,11 +9,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.calendar.CalendarRepository 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.ktx.tryStartActivity
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.widgets.CalendarWidget import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.CalendarWidgetConfig import de.mm20.launcher2.widgets.CalendarWidgetConfig
@ -34,14 +34,14 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
private val calendarRepository: CalendarRepository by inject() private val calendarRepository: CalendarRepository by inject()
private val favoritesService: FavoritesService 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()) private val widgetConfig = MutableStateFlow(CalendarWidgetConfig())
val calendarEvents = mutableStateOf<List<CalendarEvent>>(emptyList()) val calendarEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
val pinnedCalendarEvents = val pinnedCalendarEvents =
favoritesService.getFavorites( favoritesService.getFavorites(
includeTypes = listOf(CalendarEvent.Domain), includeTypes = listOf("calendar"),
automaticallySorted = true, automaticallySorted = true,
manuallySorted = true, manuallySorted = true,
).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) ).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
@ -164,12 +164,14 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
suspend fun onActive() { suspend fun onActive() {
selectDate(LocalDate.now()) selectDate(LocalDate.now())
widgetConfig.collectLatest { config -> widgetConfig.collectLatest { config ->
calendarRepository.getUpcomingEvents( calendarRepository.findMany(
from = System.currentTimeMillis(),
to = System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000L,
excludeAllDayEvents = !config.allDayEvents, excludeAllDayEvents = !config.allDayEvents,
excludeCalendars = config.excludedCalendarIds, excludeCalendars = config.excludedCalendarIds,
).collectLatest { events -> ).collectLatest { events ->
searchableRepository.getKeys( searchableRepository.getKeys(
includeTypes = listOf(CalendarEvent.Domain), includeTypes = listOf("calendar"),
hidden = true, hidden = true,
limit = 9999, limit = 9999,
).collectLatest { hidden -> ).collectLatest { hidden ->

View File

@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.searchable.SearchableRepository
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService

View File

@ -3,13 +3,13 @@ package de.mm20.launcher2.ui.settings.debug
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.icons.IconService 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.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
class DebugSettingsScreenVM: ViewModel(), KoinComponent { class DebugSettingsScreenVM: ViewModel(), KoinComponent {
private val searchableRepository: SearchableRepository by inject() private val searchableRepository: SavableSearchableRepository by inject()
private val customAttributesRepository: CustomAttributesRepository by inject() private val customAttributesRepository: CustomAttributesRepository by inject()
private val iconService: IconService by inject() private val iconService: IconService by inject()
suspend fun cleanUpDatabase(): Int { suspend fun cleanUpDatabase(): Int {

View File

@ -3,7 +3,7 @@ package de.mm20.launcher2.ui.settings.gestures
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.IconService
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
@ -24,7 +24,7 @@ import org.koin.core.component.inject
class GestureSettingsScreenVM : ViewModel(), KoinComponent { class GestureSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val searchableRepository: SearchableRepository by inject() private val searchableRepository: SavableSearchableRepository by inject()
private val iconService: IconService by inject() private val iconService: IconService by inject()
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility) val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility)

View File

@ -1,20 +1,17 @@
package de.mm20.launcher2.ui.settings.hiddenitems package de.mm20.launcher2.ui.settings.hiddenitems
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.pm.LauncherApps
import android.os.Bundle import android.os.Bundle
import androidx.core.content.getSystemService
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.applications.AppRepository 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.IconService
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.Application
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@ -30,16 +27,16 @@ import org.koin.core.component.inject
class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent { class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
private val appRepository: AppRepository by inject() private val appRepository: AppRepository by inject()
private val searchableRepository: SearchableRepository by inject() private val searchableRepository: SavableSearchableRepository by inject()
private val iconService: IconService by inject() private val iconService: IconService by inject()
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
val allApps = appRepository.getAllInstalledApps().map { val allApps = appRepository.findMany().map {
withContext(Dispatchers.Default) { it.sorted() } withContext(Dispatchers.Default) { it.sorted() }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val hiddenItems: StateFlow<List<SavableSearchable>> = flow { val hiddenItems: StateFlow<List<SavableSearchable>> = flow {
val hidden = val hidden =
searchableRepository.get(hidden = true).first().filter { it !is LauncherApp }.sorted() searchableRepository.get(hidden = true).first().filter { it !is Application }.sorted()
emit(hidden) emit(hidden)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
@ -67,15 +64,8 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
searchable.launch(context, bundle) searchable.launch(context, bundle)
} }
fun openAppInfo(context: Context, app: LauncherApp) { fun openAppInfo(context: Context, app: Application) {
val launcherApps = context.getSystemService<LauncherApps>()!! app.openAppDetails(context)
launcherApps.startAppDetailsActivity(
ComponentName(app.`package`, app.activity),
app.getUser(),
null,
null
)
} }
val hiddenItemsButton = dataStore.data.map { it.searchBar.hiddenItemsButton } val hiddenItemsButton = dataStore.data.map { it.searchBar.hiddenItemsButton }

View File

@ -12,7 +12,7 @@ import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings 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 de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -224,7 +224,7 @@ class IconsSettingsScreenVM(
fun getPreviewIcons(size: Int): Flow<List<LauncherIcon?>> { fun getPreviewIcons(size: Int): Flow<List<LauncherIcon?>> {
return columnCount.flatMapLatest { cols -> return columnCount.flatMapLatest { cols ->
favoritesService.getFavorites( favoritesService.getFavorites(
includeTypes = listOf(LauncherApp.Domain), includeTypes = listOf("app"),
limit = cols, limit = cols,
manuallySorted = true, manuallySorted = true,
automaticallySorted = true, automaticallySorted = true,

View File

@ -12,6 +12,7 @@ import de.mm20.launcher2.music.MusicService
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.AppProfile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@ -49,8 +50,8 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
loading.value = true loading.value = true
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
val musicApps = musicService.getInstalledPlayerPackages() val musicApps = musicService.getInstalledPlayerPackages()
val allApps = appRepository.getAllInstalledApps().first().filter { it.isMainProfile } val allApps = appRepository.findMany().first().filter { it.profile == AppProfile.Personal }
.distinctBy { it.`package` } .distinctBy { it.componentName.packageName }
val settings = dataStore.data.map { it.musicWidget }.first() val settings = dataStore.data.map { it.musicWidget }.first()
val allowList = settings.allowListList val allowList = settings.allowListList
val denyList = settings.denyListList val denyList = settings.denyListList
@ -58,10 +59,10 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
appList.value = allApps.map { appList.value = allApps.map {
AppListItem( AppListItem(
label = it.label, label = it.label,
packageName = it.`package`, packageName = it.componentName.packageName,
isMusicApp = musicApps.contains(it.`package`), isMusicApp = musicApps.contains(it.componentName.packageName),
isChecked = allowList.contains(it.`package`) || (!denyList.contains(it.`package`) && musicApps.contains( isChecked = allowList.contains(it.componentName.packageName) || (!denyList.contains(it.componentName.packageName) && musicApps.contains(
it.`package` it.componentName.packageName
)), )),
icon = iconService.getIcon(it, (32 * density).roundToInt()) icon = iconService.getIcon(it, (32 * density).roundToInt())
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(10000)) .shareIn(viewModelScope, SharingStarted.WhileSubscribed(10000))

View File

@ -55,7 +55,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
allTags = tagService.getAllTags().first().toSet() allTags = tagService.getAllTags().first().toSet()
val items = if (tag != null) tagService.getTaggedItems(tag).first() else emptyList() 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 taggedItems = items
taggableApps = apps.map { app -> TaggableItem(app, items.any { app.key == it.key }) } taggableApps = apps.map { app -> TaggableItem(app, items.any { app.key == it.key }) }
taggableOther = items.mapNotNull { item -> taggableOther = items.mapNotNull { item ->

View File

@ -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.Context
import android.content.Intent
import androidx.core.content.ContextCompat 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.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.search.SavableSearchable
interface AppShortcut: SavableSearchable { interface AppShortcut : SavableSearchable {
val appName: String? val appName: String?
val componentName: ComponentName?
val packageName: String?
override val preferDetailsOverLaunch: Boolean override val preferDetailsOverLaunch: Boolean
get() = false get() = false
@ -27,10 +28,13 @@ interface AppShortcut: SavableSearchable {
) )
} }
companion object { val canDelete: Boolean
fun fromPinRequestIntent(context: Context, data: Intent): AppShortcut? { get() = false
return LauncherShortcut.fromPinRequestIntent(context, data)
?: LegacyShortcut.fromPinRequestIntent(context, data) suspend fun delete(context: Context) {}
}
} val isUnavailable: Boolean
get() = false
val profile: AppProfile
} }

View File

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

View File

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

View File

@ -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<String>
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) {}
}

View File

@ -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<ContactInfo>
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
}

View File

@ -1,13 +1,12 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.search
import android.content.Context import android.content.Context
import androidx.core.content.ContextCompat 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.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.search.SavableSearchable import java.util.Locale
import java.util.*
interface File : SavableSearchable { interface File : SavableSearchable {
val path: String val path: String
@ -21,7 +20,7 @@ interface File : SavableSearchable {
override val preferDetailsOverLaunch: Boolean override val preferDetailsOverLaunch: Boolean
get() = false get() = false
open val providerIconRes: Int? val providerIconRes: Int?
get() = null get() = null
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {

View File

@ -8,8 +8,6 @@ import de.mm20.launcher2.ktx.romanize
import java.text.Collator import java.text.Collator
interface SavableSearchable : Searchable, Comparable<SavableSearchable> { interface SavableSearchable : Searchable, Comparable<SavableSearchable> {
val domain: String
val key: String val key: String
val label: String val label: String
@ -41,4 +39,11 @@ interface SavableSearchable : Searchable, Comparable<SavableSearchable> {
.compare(label1.romanize(), label2.romanize()) .compare(label1.romanize(), label2.romanize())
} }
val domain: String
fun getSerializer(): SearchableSerializer
interface Companion {
val Domain: String
}
} }

View File

@ -1,11 +1,11 @@
package de.mm20.launcher2.search package de.mm20.launcher2.search
interface SearchableDeserializer { interface SearchableDeserializer {
fun deserialize(serialized: String): SavableSearchable? suspend fun deserialize(serialized: String): SavableSearchable?
} }
class NullDeserializer: SearchableDeserializer { class NullDeserializer: SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable? { override suspend fun deserialize(serialized: String): SavableSearchable? {
return null return null
} }

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.search
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
interface SearchableRepository<T : Searchable> {
fun search(query: String): Flow<ImmutableList<T>>
}

View File

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

View File

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

View File

@ -6,8 +6,8 @@ import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.os.Process import android.os.Process
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -16,51 +16,18 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map 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<Application> {
private val fakeApp: LauncherApp override fun search(query: String): Flow<ImmutableList<Application>> {
init {
val launcherApps = context.getSystemService<LauncherApps>()!!
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<List<LauncherApp>> {
return flowOf(buildList {
repeat(fakePackages) {
add(fakeApp.copy(`package` = randomString(), activity = randomString()))
}
})
}
override fun getSuspendedPackages(): Flow<List<String>> {
return flowOf(emptyList())
}
override fun search(query: String): Flow<ImmutableList<LauncherApp>> {
return if (query.isEmpty()) { return if (query.isEmpty()) {
getAllInstalledApps().map { it.toImmutableList() } buildList {
repeat(fakePackages) {
add(FakeApp())
}
}.toImmutableList().let { flowOf(it) }
} else { } else {
flowOf(persistentListOf()) flowOf(persistentListOf())
} }

View File

@ -10,28 +10,34 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Process import android.os.Process
import android.os.UserHandle import android.os.UserHandle
import android.util.Log
import de.mm20.launcher2.ktx.normalize 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.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore import org.apache.commons.text.similarity.FuzzyScore
import java.util.Locale import java.util.Locale
interface AppRepository { interface AppRepository : SearchableRepository<Application> {
fun getAllInstalledApps(): Flow<List<LauncherApp>> override fun search(query: String): Flow<ImmutableList<Application>>
fun getSuspendedPackages(): Flow<List<String>>
fun search(query: String): Flow<ImmutableList<LauncherApp>> fun findMany(): Flow<ImmutableList<Application>>
} }
internal class AppRepositoryImpl( internal class AppRepositoryImpl(
private val context: Context, private val context: Context,
) : AppRepository { ) : AppRepository {
private val scope = CoroutineScope(Dispatchers.Default + Job())
private val launcherApps = private val launcherApps =
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
@ -40,9 +46,10 @@ internal class AppRepositoryImpl(
private val suspendedPackages = MutableStateFlow<List<String>>(emptyList()) private val suspendedPackages = MutableStateFlow<List<String>>(emptyList())
private val profiles: List<UserHandle> = private val profiles: List<UserHandle>
launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle()) get() = launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
private val mutex = Mutex()
init { init {
launcherApps.registerCallback(object : LauncherApps.Callback() { launcherApps.registerCallback(object : LauncherApps.Callback() {
@ -51,15 +58,23 @@ internal class AppRepositoryImpl(
user: UserHandle, user: UserHandle,
replacing: Boolean replacing: Boolean
) { ) {
installedApps.value = scope.launch {
installedApps.value.filter { !packageNames.contains(it.`package`) } mutex.withLock {
installedApps.value =
installedApps.value.filter { !packageNames.contains(it.componentName.packageName) }
}
}
} }
override fun onPackageChanged(packageName: String, user: UserHandle) { override fun onPackageChanged(packageName: String, user: UserHandle) {
val apps = installedApps.value.toMutableList() scope.launch {
apps.removeAll { packageName == it.`package` } mutex.withLock {
apps.addAll(getApplications(packageName)) val apps = installedApps.value.toMutableList()
installedApps.value = apps apps.removeAll { packageName == it.componentName.packageName }
apps.addAll(getApplications(packageName))
installedApps.value = apps
}
}
} }
override fun onPackagesAvailable( override fun onPackagesAvailable(
@ -67,23 +82,35 @@ internal class AppRepositoryImpl(
user: UserHandle, user: UserHandle,
replacing: Boolean replacing: Boolean
) { ) {
val apps = installedApps.value.toMutableList() scope.launch {
for (packageName in packageNames) { mutex.withLock {
apps.addAll(getApplications(packageName)) 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) { override fun onPackageAdded(packageName: String, user: UserHandle) {
Log.d("MM20", "App installed: $packageName") scope.launch {
val apps = installedApps.value.toMutableList() mutex.withLock {
apps.addAll(getApplications(packageName)) val apps = installedApps.value.toMutableList()
installedApps.value = apps apps.addAll(getApplications(packageName))
installedApps.value = apps
}
}
} }
override fun onPackageRemoved(packageName: String, user: UserHandle) { override fun onPackageRemoved(packageName: String, user: UserHandle) {
installedApps.value = scope.launch {
installedApps.value.filter { packageName != (it.`package`) || it.getUser() != user } mutex.withLock {
installedApps.value =
installedApps.value.filter { packageName != (it.componentName.packageName) || it.user != user }
}
}
} }
override fun onShortcutsChanged( override fun onShortcutsChanged(
@ -91,40 +118,55 @@ internal class AppRepositoryImpl(
shortcuts: MutableList<ShortcutInfo>, shortcuts: MutableList<ShortcutInfo>,
user: UserHandle user: UserHandle
) { ) {
super.onShortcutsChanged(packageName, shortcuts, user)
onPackageChanged(packageName, user) onPackageChanged(packageName, user)
} }
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) { override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
super.onPackagesSuspended(packageNames, user)
packageNames ?: return 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( override fun onPackagesUnsuspended(
packageNames: Array<out String>?, packageNames: Array<out String>?,
user: UserHandle? user: UserHandle?
) { ) {
super.onPackagesUnsuspended(packageNames, user)
packageNames ?: return packageNames ?: return
suspendedPackages.value = scope.launch {
suspendedPackages.value.filter { packageNames.contains(it) } mutex.withLock {
installedApps.value = installedApps.value.map {
if (packageNames.contains(it.componentName.packageName)) {
it.copy(isSuspended = false)
} else {
it
}
}
}
}
} }
}, Handler(Looper.getMainLooper())) }, Handler(Looper.getMainLooper()))
val apps = profiles.map { p -> scope.launch {
try { mutex.withLock {
launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) } val apps = profiles.map { p ->
} catch (e: SecurityException) { try {
emptyList() launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) }
} catch (e: SecurityException) {
emptyList()
}
}.flatten()
installedApps.value = apps
} }
}.flatten() }
installedApps.value = apps
}
override fun getSuspendedPackages(): Flow<List<String>> {
return suspendedPackages
} }
private fun getApplications(packageName: String): List<LauncherApp> { private fun getApplications(packageName: String): List<LauncherApp> {
@ -151,6 +193,10 @@ internal class AppRepositoryImpl(
return LauncherApp(context, launcherActivityInfo) return LauncherApp(context, launcherActivityInfo)
} }
override fun findMany(): Flow<ImmutableList<Application>> {
return installedApps.map { it.toImmutableList() }
}
override fun search(query: String): Flow<ImmutableList<LauncherApp>> { override fun search(query: String): Flow<ImmutableList<LauncherApp>> {
return installedApps.map { apps -> return installedApps.map { apps ->
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
@ -171,10 +217,6 @@ internal class AppRepositoryImpl(
} }
} }
override fun getAllInstalledApps(): Flow<List<LauncherApp>> {
return installedApps
}
private fun matches(label: String, query: String): Boolean { private fun matches(label: String, query: String): Boolean {
val normalizedLabel = label.normalize() val normalizedLabel = label.normalize()
val normalizedQuery = query.normalize() val normalizedQuery = query.normalize()

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.applications
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@ -15,8 +15,8 @@ class LauncherAppSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String { override fun serialize(searchable: SavableSearchable): String {
searchable as LauncherApp searchable as LauncherApp
val json = JSONObject() val json = JSONObject()
json.put("package", searchable.`package`) json.put("package", searchable.componentName.packageName)
json.put("activity", searchable.activity) json.put("activity", searchable.componentName.className)
json.put("user", searchable.userSerialNumber) json.put("user", searchable.userSerialNumber)
return json.toString() return json.toString()
} }
@ -26,7 +26,7 @@ class LauncherAppSerializer : SearchableSerializer {
} }
class LauncherAppDeserializer(val context: Context) : SearchableDeserializer { class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable? { override suspend fun deserialize(serialized: String): SavableSearchable? {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val launcherApps = context.getSystemService<LauncherApps>()!! val launcherApps = context.getSystemService<LauncherApps>()!!
val userManager = context.getSystemService<UserManager>()!! val userManager = context.getSystemService<UserManager>()!!

View File

@ -1,9 +1,10 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.applications
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
@ -13,43 +14,56 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
import android.os.UserHandle import android.os.UserHandle
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.applications.R
import de.mm20.launcher2.compat.PackageManagerCompat 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.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
data class LauncherApp( internal data class LauncherApp(
val launcherActivityInfo: LauncherActivityInfo, private val launcherActivityInfo: LauncherActivityInfo,
override val label: String, override val versionName: String?,
val `package`: String, override val isSuspended: Boolean = false,
val activity: String,
val flags: Int,
val version: String?,
internal val userSerialNumber: Long, internal val userSerialNumber: Long,
override val labelOverride: String? = null, 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, launcherActivityInfo,
label = launcherActivityInfo.label.toString(), versionName = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
`package` = launcherActivityInfo.applicationInfo.packageName,
activity = launcherActivityInfo.name,
flags = launcherActivityInfo.applicationInfo.flags,
version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
userSerialNumber = launcherActivityInfo.user.getSerialNumber(context) userSerialNumber = launcherActivityInfo.user.getSerialNumber(context)
) )
val isMainProfile = launcherActivityInfo.user == Process.myUserHandle() override val user: UserHandle
get() = launcherActivityInfo.user
val canUninstall: Boolean private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
get() = flags and ApplicationInfo.FLAG_SYSTEM == 0 && isMainProfile
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 domain: String = Domain
override val preferDetailsOverLaunch: Boolean = false override val preferDetailsOverLaunch: Boolean = false
@ -59,22 +73,10 @@ data class LauncherApp(
} }
override val key: String 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( override suspend fun loadIcon(
context: Context, context: Context,
@ -132,7 +134,7 @@ data class LauncherApp(
} }
try { try {
launcherApps.startMainActivity( launcherApps.startMainActivity(
ComponentName(`package`, activity), componentName,
launcherActivityInfo.user, launcherActivityInfo.user,
null, null,
options options
@ -145,11 +147,15 @@ data class LauncherApp(
return true return true
} }
fun getStoreDetails(context: Context): StoreLink? { override fun getStoreDetails(context: Context): StoreLink? {
val pm = context.packageManager val pm = context.packageManager
return try { return try {
val installSourceInfo = PackageManagerCompat.getInstallSource(pm, `package`) val installSourceInfo =
getStoreLinkForInstaller(installSourceInfo.initiatingPackageName, `package`) PackageManagerCompat.getInstallSource(pm, componentName.packageName)
getStoreLinkForInstaller(
installSourceInfo.initiatingPackageName,
componentName.packageName
)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
null null
} catch (e: IllegalArgumentException) { } 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) val intent = Intent(Intent.ACTION_DELETE)
intent.data = Uri.parse("package:$`package`") intent.data = Uri.parse("package:${componentName.packageName}")
context.startActivity(intent) context.startActivity(intent)
} }
fun openAppInfo(context: Context) { override fun openAppDetails(context: Context) {
val launcherApps = context.getSystemService<LauncherApps>()!! val launcherApps = context.getSystemService<LauncherApps>()!!
launcherApps.startAppDetailsActivity( launcherApps.startAppDetailsActivity(
ComponentName(`package`, activity), componentName,
getUser(), user,
null, null,
null null
) )
} }
suspend fun shareApkFile(context: Context) { override val canShareApk: Boolean = true
override suspend fun shareApkFile(context: Context) {
val launcherApps = context.getSystemService<LauncherApps>()!! val launcherApps = context.getSystemService<LauncherApps>()!!
val fileCopy = java.io.File( val fileCopy = java.io.File(
context.cacheDir, context.cacheDir,
"${`package`}-${version}.apk" "${componentName.packageName}-${versionName}.apk"
) )
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
val user = getUser() val info = launcherApps.getApplicationInfo(componentName.packageName, 0, user)
val info = if (user != null) {
launcherApps.getApplicationInfo(`package`, 0, user)
} else {
context.packageManager.getApplicationInfo(`package`, 0)
}
val file = java.io.File(info.publicSourceDir) val file = java.io.File(info.publicSourceDir)
try { 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 { companion object {
private fun getStoreLinkForInstaller( private fun getStoreLinkForInstaller(
installerPackage: String?, installerPackage: String?,
@ -225,18 +234,21 @@ data class LauncherApp(
"http://www.amazon.com/gp/mas/dl/android?p=${packageName}" "http://www.amazon.com/gp/mas/dl/android?p=${packageName}"
) )
} }
"com.android.vending" -> { "com.android.vending" -> {
StoreLink( StoreLink(
"Google Play Store", "Google Play Store",
"https://play.google.com/store/apps/details?id=${packageName}" "https://play.google.com/store/apps/details?id=${packageName}"
) )
} }
"org.fdroid.fdroid", "com.aurora.adroid" -> { "org.fdroid.fdroid", "com.aurora.adroid" -> {
StoreLink( StoreLink(
"F-Droid", "F-Droid",
"https://f-droid.org/packages/${packageName}" "https://f-droid.org/packages/${packageName}"
) )
} }
else -> null else -> null
} }
} }
@ -251,9 +263,8 @@ data class LauncherApp(
const val Domain = "app" const val Domain = "app"
} }
}
data class StoreLink( override fun getSerializer(): SearchableSerializer {
val label: String, return LauncherAppSerializer()
val url: String }
) }

View File

@ -1,8 +1,14 @@
package de.mm20.launcher2.applications 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.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val applicationsModule = module { val applicationsModule = module {
single<AppRepository> { AppRepositoryImpl(androidContext()) } factory<SearchableRepository<Application>>(named<Application>()) { AppRepositoryImpl(androidContext()) }
factory<AppRepository> { AppRepositoryImpl(androidContext()) }
factory<SearchableDeserializer>(named(LauncherApp.Domain)) { LauncherAppDeserializer(androidContext()) }
} }

View File

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

View File

@ -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<AppShortcutConfigActivity> {
val label = launcherActivityInfo.label.toString()
val profile: AppProfile = if (launcherActivityInfo.user == Process.myUserHandle()) AppProfile.Personal else AppProfile.Work
fun getIcon(context: Context): Flow<Drawable?> = flow {
val icon = launcherActivityInfo.getIcon(context.resources.displayMetrics.densityDpi)
emit(icon)
}
fun getIntent(context: Context): IntentSender? {
val launcherApps = context.getSystemService<LauncherApps>()!!
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())
}
}

View File

@ -1,21 +1,19 @@
package de.mm20.launcher2.appshortcuts package de.mm20.launcher2.appshortcuts
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo import android.content.pm.ShortcutInfo
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Process import android.os.Process
import android.os.UserHandle import android.os.UserHandle
import android.util.Log
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.normalize import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.SearchableRepository
import de.mm20.launcher2.search.data.LauncherShortcut
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -28,22 +26,25 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore import org.apache.commons.text.similarity.FuzzyScore
import java.util.Locale import java.util.Locale
interface AppShortcutRepository { interface AppShortcutRepository : SearchableRepository<AppShortcut> {
fun search(query: String): Flow<ImmutableList<AppShortcut>> fun findMany(
suspend fun getShortcutsForActivity( componentName: ComponentName? = null,
launcherActivityInfo: LauncherActivityInfo, user: UserHandle = Process.myUserHandle(),
count: Int = 5 manifest: Boolean = false,
): List<LauncherShortcut> dynamic: Boolean = false,
pinned: Boolean = false,
cached: Boolean = false,
limit: Int = 5,
): Flow<ImmutableList<AppShortcut>>
suspend fun getShortcutsConfigActivities(): List<LauncherApp> suspend fun getShortcutsConfigActivities(): List<AppShortcutConfigActivity>
fun removePinnedShortcut(shortcut: LauncherShortcut)
} }
internal class AppShortcutRepositoryImpl( internal class AppShortcutRepositoryImpl(
@ -53,33 +54,58 @@ internal class AppShortcutRepositoryImpl(
private val scope = CoroutineScope(Dispatchers.Default + Job()) private val scope = CoroutineScope(Dispatchers.Default + Job())
override suspend fun getShortcutsForActivity( override fun findMany(
launcherActivityInfo: LauncherActivityInfo, componentName: ComponentName?,
count: Int, user: UserHandle,
) = withContext(Dispatchers.IO) { manifest: Boolean,
val launcherApps = context.getSystemService<LauncherApps>()!! dynamic: Boolean,
if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList() pinned: Boolean,
val query = LauncherApps.ShortcutQuery() cached: Boolean,
.setPackage(launcherActivityInfo.applicationInfo.packageName) limit: Int
.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST) ): Flow<ImmutableList<AppShortcut>> = flow {
val shortcuts = try { val shortcuts = withContext(Dispatchers.IO) {
launcherApps.getShortcuts(query, launcherActivityInfo.user) val launcherApps = context.getSystemService<LauncherApps>()!!
} catch (e: IllegalStateException) { if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList()
emptyList() val query = LauncherApps.ShortcutQuery()
} .setActivity(componentName)
val appShortcuts = mutableListOf<LauncherShortcut>() .setQueryFlags(
appShortcuts.addAll(shortcuts buildQueryFlags(manifest, dynamic, pinned, cached)
?.let {
if (it.size > count) it.subList(0, count)
else it
}
?.map {
LauncherShortcut(
context,
it,
) )
} ?: emptyList()) val shortcuts = try {
appShortcuts launcherApps.getShortcuts(query, user)
} catch (e: IllegalStateException) {
emptyList()
}
val appShortcuts = mutableListOf<LauncherShortcut>()
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<ImmutableList<AppShortcut>> { override fun search(query: String) = channelFlow<ImmutableList<AppShortcut>> {
@ -93,7 +119,6 @@ internal class AppShortcutRepositoryImpl(
return@withContext return@withContext
} }
shortcutChangeEmitter.collectLatest { shortcutChangeEmitter.collectLatest {
val launcherApps = val launcherApps =
context.getSystemService<LauncherApps>() ?: return@collectLatest send( context.getSystemService<LauncherApps>() ?: return@collectLatest send(
@ -180,39 +205,16 @@ internal class AppShortcutRepositoryImpl(
} }
}.shareIn(scope, SharingStarted.WhileSubscribed(500), 1) }.shareIn(scope, SharingStarted.WhileSubscribed(500), 1)
override fun removePinnedShortcut(shortcut: LauncherShortcut) { override suspend fun getShortcutsConfigActivities(): List<AppShortcutConfigActivity> {
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<LauncherApp> {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return emptyList() if (!launcherApps.hasShortcutHostPermission()) return emptyList()
val results = mutableListOf<LauncherApp>() val results = mutableListOf<AppShortcutConfigActivity>()
val profiles = launcherApps.profiles val profiles = launcherApps.profiles
for (profile in profiles) { for (profile in profiles) {
val activities = launcherApps.getShortcutConfigActivityList(null, profile) val activities = launcherApps.getShortcutConfigActivityList(null, profile)
results.addAll( results.addAll(
activities.map { activities.map {
LauncherApp( AppShortcutConfigActivity(it)
context, it
)
} }
) )
} }

View File

@ -11,9 +11,6 @@ import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer 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.json.JSONObject
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -37,7 +34,7 @@ class LauncherShortcutDeserializer(
val context: Context val context: Context
) : SearchableDeserializer, KoinComponent { ) : 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 launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val json = JSONObject(serialized) val json = JSONObject(serialized)
@ -65,16 +62,9 @@ class LauncherShortcutDeserializer(
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
return null return null
} }
val pm = context.packageManager if (shortcuts.isNullOrEmpty()) {
val appName = try {
pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString()
} catch (e: PackageManager.NameNotFoundException) {
return null
}
if (shortcuts == null || shortcuts.isEmpty()) {
return null return null
} else { } else {
val activity = shortcuts[0].activity
return LauncherShortcut( return LauncherShortcut(
context = context, context = context,
launcherShortcut = shortcuts[0], 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 { class LegacyShortcutSerializer: SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String { override fun serialize(searchable: SavableSearchable): String {
searchable as LegacyShortcut searchable as LegacyShortcut
@ -106,7 +111,7 @@ class LegacyShortcutSerializer: SearchableSerializer {
class LegacyShortcutDeserializer( class LegacyShortcutDeserializer(
val context: Context val context: Context
): SearchableDeserializer { ): SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable { override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val label = json.getString("label") val label = json.getString("label")
val intent = Intent.parseUri(json.getString("intent"), 0) val intent = Intent.parseUri(json.getString("intent"), 0)

View File

@ -1,6 +1,7 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.appshortcuts
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
@ -12,11 +13,13 @@ import android.os.Process
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.icons.* import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.NullPointerException import java.lang.NullPointerException
@ -24,7 +27,7 @@ import java.lang.NullPointerException
/** /**
* Represents a modern (Android O+) launcher shortcut * Represents a modern (Android O+) launcher shortcut
*/ */
data class LauncherShortcut( internal data class LauncherShortcut(
val launcherShortcut: ShortcutInfo, val launcherShortcut: ShortcutInfo,
override val appName: String?, override val appName: String?,
internal val userSerialNumber: Long, internal val userSerialNumber: Long,
@ -32,6 +35,11 @@ data class LauncherShortcut(
) : AppShortcut { ) : AppShortcut {
override val domain: String = Domain override val domain: String = Domain
override val componentName: ComponentName?
get() = launcherShortcut.activity
override val packageName: String
get() = launcherShortcut.`package`
constructor( constructor(
context: Context, context: Context,
@ -48,7 +56,7 @@ data class LauncherShortcut(
) )
override val label: String override val label: String
get() = launcherShortcut.shortLabel?.toString() ?: "" get() = launcherShortcut.shortLabel?.toString() ?: launcherShortcut.longLabel?.toString() ?: ""
override fun overrideLabel(label: String): LauncherShortcut { override fun overrideLabel(label: String): LauncherShortcut {
return this.copy(labelOverride = label) return this.copy(labelOverride = label)
@ -56,8 +64,10 @@ data class LauncherShortcut(
override val preferDetailsOverLaunch: Boolean = false 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 override val key: String
get() = if (isMainProfile) { 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 { companion object {
fun fromPinRequestIntent(context: Context, data: Intent): LauncherShortcut? { fun fromPinRequestIntent(context: Context, data: Intent): LauncherShortcut? {
val launcherApps = val launcherApps =

View File

@ -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.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.ShortcutIconResource 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.getDrawableOrNull
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity 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, val intent: Intent,
override val label: String, override val label: String,
override val appName: String?, override val appName: String?,
@ -22,6 +26,9 @@ data class LegacyShortcut(
override val domain = Domain override val domain = Domain
override val key: String = "$domain://${intent.toUri(0)}" override val key: String = "$domain://${intent.toUri(0)}"
override val profile: AppProfile
get() = AppProfile.Personal
override fun overrideLabel(label: String): LegacyShortcut { override fun overrideLabel(label: String): LegacyShortcut {
return this.copy(labelOverride = label) return this.copy(labelOverride = label)
} }
@ -31,7 +38,10 @@ data class LegacyShortcut(
return context.tryStartActivity(intent, options) 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 get() = intent.`package` ?: intent.component?.packageName
override suspend fun loadIcon(context: Context, size: Int, themed: Boolean): LauncherIcon? { 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 { companion object {
const val Domain = "legacyshortcut" const val Domain = "legacyshortcut"

View File

@ -1,8 +1,15 @@
package de.mm20.launcher2.appshortcuts 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.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val appShortcutsModule = module { val appShortcutsModule = module {
single<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get()) } factory<SearchableRepository<AppShortcut>>(named<AppShortcut>()) { AppShortcutRepositoryImpl(androidContext(), get()) }
factory<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext(), get()) }
factory<SearchableDeserializer>(named(LauncherShortcut.Domain)) { LauncherShortcutDeserializer(androidContext()) }
factory<SearchableDeserializer>(named(LegacyShortcut.Domain)) { LegacyShortcutDeserializer(androidContext()) }
} }

View File

@ -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.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
import android.os.UserManager import android.os.UserManager
import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer 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.SavableSearchable
import de.mm20.launcher2.search.SearchableSerializer
/** /**
* Shortcut class that is used when a [LauncherShortcut] is not available, e.g. missing permissions * Shortcut class that is used when a [LauncherShortcut] is not available, e.g. missing permissions
* when Kvaesitso is not set as default launcher. * when Kvaesitso is not set as default launcher.
*/ */
class UnavailableShortcut( internal class UnavailableShortcut(
override val label: String, override val label: String,
override val appName: String?, override val appName: String?,
val packageName: String, override val packageName: String,
val shortcutId: String, val shortcutId: String,
val isMainProfile: Boolean, val isMainProfile: Boolean,
val userSerial: Long, val userSerial: Long,
@ -34,6 +37,8 @@ class UnavailableShortcut(
override val labelOverride: String? override val labelOverride: String?
get() = null get() = null
override val componentName: ComponentName?
get() = null
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon( return StaticLauncherIcon(
@ -56,6 +61,14 @@ class UnavailableShortcut(
return false return false
} }
override fun getSerializer(): SearchableSerializer {
return UnavailableShortcutSerializer()
}
override val isUnavailable: Boolean = true
override val profile: AppProfile
get() = TODO("Not yet implemented")
companion object { companion object {
internal operator fun invoke(context: Context, id: String, packageName: String, userSerial: Long): UnavailableShortcut? { internal operator fun invoke(context: Context, id: String, packageName: String, userSerial: Long): UnavailableShortcut? {
val appInfo = try { val appInfo = try {

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.calendar
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
@ -6,50 +6,33 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.CalendarContract 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.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.net.URLEncoder
import java.text.SimpleDateFormat
data class CalendarEvent( internal data class AndroidCalendarEvent(
override val label: String, override val label: String,
val id: Long, val id: Long,
val color: Int, override val color: Int,
val startTime: Long, override val startTime: Long,
val endTime: Long, override val endTime: Long,
val allDay: Boolean, override val allDay: Boolean,
val location: String, override val location: String?,
val attendees: List<String>, override val attendees: List<String>,
val description: String, override val description: String?,
val calendar: Long, val calendar: Long,
override val labelOverride: String? = null, override val labelOverride: String? = null,
) : SavableSearchable { ) : CalendarEvent {
override val domain: String = Domain override val domain: String = Domain
override val key: String override val key: String
get() = "$domain://$id" get() = "$domain://$id"
override fun overrideLabel(label: String): AndroidCalendarEvent {
override val preferDetailsOverLaunch: Boolean = true
override fun overrideLabel(label: String): CalendarEvent {
return this.copy(labelOverride = label) 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 { private fun getLaunchIntent(): Intent {
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id) val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id)
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 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) return context.tryStartActivity(getLaunchIntent(), options)
} }
fun openLocation(context: Context) { override fun openLocation(context: Context) {
if (location == null) return
context.tryStartActivity( context.tryStartActivity(
Intent(Intent.ACTION_VIEW) Intent(Intent.ACTION_VIEW)
.setData( .setData(
@ -76,6 +60,10 @@ data class CalendarEvent(
) )
} }
override fun getSerializer(): SearchableSerializer {
return CalendarEventSerializer()
}
companion object { companion object {
const val Domain = "calendar" const val Domain = "calendar"
} }

View File

@ -6,8 +6,8 @@ import android.provider.CalendarContract
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.data.UserCalendar import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -18,16 +18,16 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import java.util.Calendar import java.util.Calendar
interface CalendarRepository { interface CalendarRepository: SearchableRepository<CalendarEvent> {
fun findMany(
fun search(query: String): Flow<ImmutableList<CalendarEvent>> from: Long = System.currentTimeMillis(),
fun getUpcomingEvents( to: Long = from + 14 * 24 * 60 * 60 * 1000L,
excludeCalendars: List<Long>, excludeCalendars: List<Long> = emptyList(),
excludeAllDayEvents: Boolean excludeAllDayEvents: Boolean = false,
): Flow<List<CalendarEvent>> limit: Int = 999,
): Flow<ImmutableList<CalendarEvent>>
suspend fun getCalendars(): List<UserCalendar> suspend fun getCalendars(): List<UserCalendar>
} }
@ -60,16 +60,43 @@ internal class CalendarRepositoryImpl(
} }
override fun findMany(
from: Long,
to: Long,
excludeCalendars: List<Long>,
excludeAllDayEvents: Boolean,
limit: Int,
) = channelFlow<ImmutableList<CalendarEvent>> {
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( private suspend fun queryCalendarEvents(
query: String, query: String?,
intervalStart: Long, intervalStart: Long,
intervalEnd: Long, intervalEnd: Long,
limit: Int = 10, limit: Int = 10,
excludeAllDayEvents: Boolean = false, excludeAllDayEvents: Boolean = false,
excludeCalendars: List<Long> = emptyList(), excludeCalendars: List<Long> = emptyList(),
): List<CalendarEvent> { ): List<AndroidCalendarEvent> {
val results = withContext(Dispatchers.IO) { val results = withContext(Dispatchers.IO) {
val results = mutableListOf<CalendarEvent>() val results = mutableListOf<AndroidCalendarEvent>()
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon() val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(builder, intervalStart) ContentUris.appendId(builder, intervalStart)
ContentUris.appendId(builder, intervalEnd) ContentUris.appendId(builder, intervalEnd)
@ -86,10 +113,10 @@ internal class CalendarRepositoryImpl(
CalendarContract.Instances.DESCRIPTION CalendarContract.Instances.DESCRIPTION
) )
val selection = mutableListOf<String>() val selection = mutableListOf<String>()
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 (excludeCalendars.isNotEmpty()) selection.add("${CalendarContract.Instances.CALENDAR_ID} NOT IN (${excludeCalendars.joinToString()})")
if (excludeAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0") 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 = val sort =
"${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else "" "${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else ""
val cursor = context.contentResolver.query( val cursor = context.contentResolver.query(
@ -128,7 +155,7 @@ internal class CalendarRepositoryImpl(
} else { } else {
0 0
} }
val event = CalendarEvent( val event = AndroidCalendarEvent(
label = cursor.getStringOrNull(1) ?: "", label = cursor.getStringOrNull(1) ?: "",
id = cursor.getLong(0), id = cursor.getLong(0),
color = cursor.getInt(5), color = cursor.getInt(5),
@ -150,32 +177,6 @@ internal class CalendarRepositoryImpl(
return results return results
} }
override fun getUpcomingEvents(
excludeCalendars: List<Long>,
excludeAllDayEvents: Boolean,
): Flow<List<CalendarEvent>> = 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<UserCalendar> { override suspend fun getCalendars(): List<UserCalendar> {
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Calendar)) return emptyList() if (!permissionsManager.checkPermissionOnce(PermissionGroup.Calendar)) return emptyList()
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {

View File

@ -10,13 +10,12 @@ import androidx.core.database.getStringOrNull
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.CalendarEvent
import org.json.JSONObject import org.json.JSONObject
import java.util.* import java.util.*
class CalendarEventSerializer: SearchableSerializer { class CalendarEventSerializer: SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String { override fun serialize(searchable: SavableSearchable): String {
searchable as CalendarEvent searchable as AndroidCalendarEvent
val json = JSONObject() val json = JSONObject()
json.put("id", searchable.id) json.put("id", searchable.id)
return json.toString() return json.toString()
@ -27,7 +26,7 @@ class CalendarEventSerializer: SearchableSerializer {
} }
class CalendarEventDeserializer(val context: Context): SearchableDeserializer { 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 if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
val json = JSONObject(serialized) val json = JSONObject(serialized)
val id = json.getLong("id") val id = json.getLong("id")
@ -86,7 +85,7 @@ class CalendarEventDeserializer(val context: Context): SearchableDeserializer {
} else { } else {
0 0
} }
return CalendarEvent( return AndroidCalendarEvent(
label = title, label = title,
id = id, id = id,
color = color, color = color,

View File

@ -1,8 +1,14 @@
package de.mm20.launcher2.calendar 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.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val calendarModule = module { val calendarModule = module {
single<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get()) } factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { CalendarRepositoryImpl(androidContext(), get()) }
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get()) }
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { CalendarEventDeserializer(androidContext()) }
} }

View File

@ -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<ContactInfo>,
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"
}
}

View File

@ -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"
}
}

View File

@ -2,9 +2,12 @@ package de.mm20.launcher2.contacts
import android.content.Context import android.content.Context
import android.provider.ContactsContract import android.provider.ContactsContract
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.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.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -12,14 +15,143 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
interface ContactRepository { internal class ContactRepository(
fun search(query: String): Flow<ImmutableList<Contact>>
}
internal class ContactRepositoryImpl(
private val context: Context, private val context: Context,
private val permissionsManager: PermissionsManager private val permissionsManager: PermissionsManager
) : ContactRepository { ) : SearchableRepository<Contact> {
fun get(id: Long): Flow<Contact?> = 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<Long>()
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<Long>): 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<ContactInfo>()
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<ImmutableList<Contact>> { override fun search(query: String): Flow<ImmutableList<Contact>> {
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts) val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
@ -45,7 +177,8 @@ internal class ContactRepositoryImpl(
ContactsContract.RawContacts.CONTACT_ID, ContactsContract.RawContacts.CONTACT_ID,
ContactsContract.RawContacts._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 selArgs = arrayOf("%$query%", "%$query%", "%$query%")
val cursor = context.contentResolver.query( val cursor = context.contentResolver.query(
ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null
@ -58,7 +191,7 @@ internal class ContactRepositoryImpl(
cursor.close() cursor.close()
val results = mutableListOf<Contact>() val results = mutableListOf<Contact>()
for ((id, rawIds) in contactMap) { 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 if (results.size > 15) break
} }
results results

View File

@ -1,20 +1,17 @@
package de.mm20.launcher2.contacts 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.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.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Contact import kotlinx.coroutines.flow.first
import org.json.JSONObject import org.json.JSONObject
class ContactSerializer : SearchableSerializer { internal class ContactSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String { override fun serialize(searchable: SavableSearchable): String {
searchable as Contact searchable as AndroidContact
return jsonObjectOf( return jsonObjectOf(
"id" to searchable.id "id" to searchable.id
).toString() ).toString()
@ -24,28 +21,15 @@ class ContactSerializer : SearchableSerializer {
get() = "contact" get() = "contact"
} }
class ContactDeserializer(val context: Context) : SearchableDeserializer { internal class ContactDeserializer(
override fun deserialize(serialized: String): SavableSearchable? { private val contactRepository: ContactRepository,
if (ContextCompat.checkSelfPermission( private val permissionsManager: PermissionsManager
context, ) : SearchableDeserializer {
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<Long>()
while (rawContactsCursor.moveToNext()) {
rawContacts.add(rawContactsCursor.getLong(0))
}
rawContactsCursor.close()
if (rawContacts.isEmpty()) return null
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()
} }
} }

View File

@ -1,8 +1,14 @@
package de.mm20.launcher2.contacts 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.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val contactsModule = module { val contactsModule = module {
single<ContactRepository> { ContactRepositoryImpl(androidContext(), get()) } factory { ContactRepository(androidContext(), get()) }
factory<SearchableRepository<Contact>>(named<Contact>()) { ContactRepository(androidContext(), get()) }
factory<SearchableDeserializer>(named(AndroidContact.Domain)) { ContactDeserializer(get(), get()) }
} }

View File

@ -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<ContactInfo>,
val emails: Set<ContactInfo>,
val telegram: Set<ContactInfo>,
val whatsapp: Set<ContactInfo>,
val signal: Set<ContactInfo>,
val postals: Set<ContactInfo>,
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<Long>): 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<ContactInfo>()
val emails = mutableSetOf<ContactInfo>()
val telegram = mutableSetOf<ContactInfo>()
val whatsapp = mutableSetOf<ContactInfo>()
val signal = mutableSetOf<ContactInfo>()
val postals = mutableSetOf<ContactInfo>()
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
)

View File

@ -1,10 +1,9 @@
package de.mm20.launcher2.data.customattrs package de.mm20.launcher2.data.customattrs
import android.database.sqlite.SQLiteDatabase
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.CustomAttributeEntity import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.searchable.SearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -48,7 +47,7 @@ interface CustomAttributesRepository {
internal class CustomAttributesRepositoryImpl( internal class CustomAttributesRepositoryImpl(
private val appDatabase: AppDatabase, private val appDatabase: AppDatabase,
private val searchableRepository: SearchableRepository private val searchableRepository: SavableSearchableRepository
) : CustomAttributesRepository { ) : CustomAttributesRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default) private val scope = CoroutineScope(Job() + Dispatchers.Default)

View File

@ -51,4 +51,5 @@ dependencies {
implementation(project(":core:i18n")) implementation(project(":core:i18n"))
implementation(project(":core:permissions")) implementation(project(":core:permissions"))
implementation(project(":core:crashreporter")) implementation(project(":core:crashreporter"))
implementation(project(":core:preferences"))
} }

View File

@ -3,18 +3,22 @@ package de.mm20.launcher2.files
import android.content.Context import android.content.Context
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.database.getStringOrNull 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.ktx.jsonObjectOf
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.*
import org.json.JSONObject import org.json.JSONObject
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
class LocalFileSerializer : SearchableSerializer { internal class LocalFileSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String { override fun serialize(searchable: SavableSearchable): String {
searchable as LocalFile searchable as LocalFile
return jsonObjectOf( return jsonObjectOf(
@ -26,10 +30,10 @@ class LocalFileSerializer : SearchableSerializer {
get() = "file" get() = "file"
} }
class LocalFileDeserializer( internal class LocalFileDeserializer(
val context: Context val context: Context
) : SearchableDeserializer, KoinComponent { ) : SearchableDeserializer, KoinComponent {
override fun deserialize(serialized: String): SavableSearchable? { override suspend fun deserialize(serialized: String): SavableSearchable? {
val permissionsManager: PermissionsManager = get() val permissionsManager: PermissionsManager = get()
if (!permissionsManager.checkPermissionOnce( if (!permissionsManager.checkPermissionOnce(
PermissionGroup.ExternalStorage PermissionGroup.ExternalStorage
@ -73,7 +77,7 @@ class LocalFileDeserializer(
} }
} }
class GDriveFileSerializer : SearchableSerializer { internal class GDriveFileSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String { override fun serialize(searchable: SavableSearchable): String {
searchable as GDriveFile searchable as GDriveFile
return jsonObjectOf( return jsonObjectOf(
@ -102,8 +106,8 @@ class GDriveFileSerializer : SearchableSerializer {
get() = "gdrive" get() = "gdrive"
} }
class GDriveFileDeserializer : SearchableDeserializer { internal class GDriveFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable { override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val id = json.getString("id") val id = json.getString("id")
val label = json.getString("label") 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 { override fun serialize(searchable: SavableSearchable): String {
searchable as OneDriveFile searchable as OneDriveFile
return jsonObjectOf( return jsonObjectOf(
@ -160,8 +164,8 @@ class OneDriveFileSerializer : SearchableSerializer {
get() = "onedrive" get() = "onedrive"
} }
class OneDriveFileDeserializer : SearchableDeserializer { internal class OneDriveFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable { override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val fileId = json.getString("id") val fileId = json.getString("id")
val label = json.getString("label") 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 { override fun serialize(searchable: SavableSearchable): String {
searchable as NextcloudFile searchable as NextcloudFile
return jsonObjectOf( return jsonObjectOf(
@ -215,8 +219,8 @@ class NextcloudFileSerializer : SearchableSerializer {
get() = "nextcloud" get() = "nextcloud"
} }
class NextcloudFileDeserializer : SearchableDeserializer { internal class NextcloudFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable { override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val id = json.getLong("id") val id = json.getLong("id")
val label = json.getString("label") 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 { override fun serialize(searchable: SavableSearchable): String {
searchable as OwncloudFile searchable as OwncloudFile
return jsonObjectOf( return jsonObjectOf(
@ -268,8 +272,8 @@ class OwncloudFileSerializer : SearchableSerializer {
get() = "owncloud" get() = "owncloud"
} }
class OwncloudFileDeserializer : SearchableDeserializer { internal class OwncloudFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable { override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val id = json.getLong("id") val id = json.getLong("id")
val label = json.getString("label") val label = json.getString("label")

View File

@ -9,34 +9,23 @@ import de.mm20.launcher2.files.providers.OwncloudFileProvider
import de.mm20.launcher2.nextcloud.NextcloudApiHelper import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.collections.immutable.ImmutableList import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
interface FileRepository { internal class FileRepository(
fun search(
query: String,
local: Boolean = true,
gdrive: Boolean = true,
onedrive: Boolean = true,
nextcloud: Boolean = true,
owncloud: Boolean = true,
): Flow<ImmutableList<File>>
fun deleteFile(file: File)
}
internal class FileRepositoryImpl(
private val context: Context, private val context: Context,
private val permissionsManager: PermissionsManager, private val permissionsManager: PermissionsManager,
) : FileRepository { private val dataStore: LauncherDataStore,
) : SearchableRepository<File> {
private val scope = CoroutineScope(Job() + Dispatchers.Default) private val scope = CoroutineScope(Job() + Dispatchers.Default)
@ -49,39 +38,28 @@ internal class FileRepositoryImpl(
override fun search( override fun search(
query: String, query: String,
local: Boolean,
gdrive: Boolean,
onedrive: Boolean,
nextcloud: Boolean,
owncloud: Boolean
) = channelFlow { ) = channelFlow {
if (query.isBlank()) { if (query.isBlank()) {
send(persistentListOf()) send(persistentListOf())
return@channelFlow return@channelFlow
} }
val providers = mutableListOf<FileProvider>() dataStore.data.map { it.fileSearch }.collectLatest {
val providers = mutableListOf<FileProvider>()
if (local) providers.add(LocalFileProvider(context, permissionsManager)) if (it.localFiles) providers.add(LocalFileProvider(context, permissionsManager))
if (gdrive) providers.add(GDriveFileProvider(context)) if (it.gdrive) providers.add(GDriveFileProvider(context))
if (nextcloud) providers.add(NextcloudFileProvider(nextcloudClient)) if (it.nextcloud) providers.add(NextcloudFileProvider(nextcloudClient))
if (owncloud) providers.add(OwncloudFileProvider(owncloudClient)) if (it.owncloud) providers.add(OwncloudFileProvider(owncloudClient))
if (providers.isEmpty()) { if (providers.isEmpty()) {
send(persistentListOf()) send(persistentListOf())
return@channelFlow return@collectLatest
} }
val results = mutableListOf<File>() val results = mutableListOf<File>()
for (provider in providers) { for (provider in providers) {
results.addAll(provider.search(query)) results.addAll(provider.search(query))
send(results.toImmutableList()) send(results.toImmutableList())
}
}
override fun deleteFile(file: File) {
scope.launch {
if (file.isDeletable) {
file.delete(context)
} }
} }
} }

View File

@ -1,8 +1,22 @@
package de.mm20.launcher2.files 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.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val filesModule = module { val filesModule = module {
single<FileRepository> { FileRepositoryImpl(androidContext(), get()) } factory<SearchableRepository<File>>(named<File>()) { FileRepository(androidContext(), get(), get()) }
factory<SearchableDeserializer>(named(LocalFile.Domain)) { LocalFileDeserializer(androidContext()) }
factory<SearchableDeserializer>(named(OwncloudFile.Domain)) { OwncloudFileDeserializer() }
factory<SearchableDeserializer>(named(NextcloudFile.Domain)) { NextcloudFileDeserializer() }
factory<SearchableDeserializer>(named(OneDriveFile.Domain)) { OneDriveFileDeserializer() }
factory<SearchableDeserializer>(named(GDriveFile.Domain)) { GDriveFileDeserializer() }
} }

View File

@ -1,7 +1,7 @@
package de.mm20.launcher2.files.providers 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<File> suspend fun search(query: String): List<File>
} }

View File

@ -1,13 +1,16 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.files.providers
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import de.mm20.launcher2.files.GDriveFileSerializer
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity 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, val fileId: String,
override val label: String, override val label: String,
override val path: String, override val path: String,
@ -43,6 +46,10 @@ data class GDriveFile(
return context.tryStartActivity(getLaunchIntent(), options) return context.tryStartActivity(getLaunchIntent(), options)
} }
override fun getSerializer(): SearchableSerializer {
return GDriveFileSerializer()
}
companion object { companion object {
const val Domain = "gdrive" const val Domain = "gdrive"
} }

View File

@ -4,8 +4,7 @@ import android.content.Context
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.gservices.DriveFileMeta import de.mm20.launcher2.gservices.DriveFileMeta
import de.mm20.launcher2.gservices.GoogleApiHelper import de.mm20.launcher2.gservices.GoogleApiHelper
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.data.GDriveFile
internal class GDriveFileProvider( internal class GDriveFileProvider(
private val context: Context private val context: Context

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.files.providers
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -16,17 +16,21 @@ import android.util.Size
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.files.LocalFileSerializer
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.* import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.formatToString import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.media.ThumbnailUtilsCompat 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.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
import java.io.File as JavaIOFile import java.io.File as JavaIOFile
data class LocalFile( internal data class LocalFile(
val id: Long, val id: Long,
override val path: String, override val path: String,
override val mimeType: String, override val mimeType: String,
@ -186,7 +190,7 @@ data class LocalFile(
val file = java.io.File(path) val file = java.io.File(path)
withContext(Dispatchers.IO) { withContext(NonCancellable + Dispatchers.IO) {
file.deleteRecursively() file.deleteRecursively()
context.contentResolver.delete( context.contentResolver.delete(
@ -352,4 +356,8 @@ data class LocalFile(
shareIntent.type = mimeType shareIntent.type = mimeType
context.startActivity(Intent.createChooser(shareIntent, null)) context.startActivity(Intent.createChooser(shareIntent, null))
} }
override fun getSerializer(): SearchableSerializer {
return LocalFileSerializer()
}
} }

View File

@ -1,13 +1,11 @@
package de.mm20.launcher2.files.providers package de.mm20.launcher2.files.providers
import android.content.Context import android.content.Context
import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.data.LocalFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext

View File

@ -1,14 +1,17 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.files.providers
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import de.mm20.launcher2.files.NextcloudFileSerializer
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity 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, val fileId: Long,
override val label: String, override val label: String,
override val path: String, override val path: String,
@ -45,6 +48,10 @@ data class NextcloudFile(
return context.tryStartActivity(getLaunchIntent(context), options) return context.tryStartActivity(getLaunchIntent(context), options)
} }
override fun getSerializer(): SearchableSerializer {
return NextcloudFileSerializer()
}
companion object { companion object {
const val Domain = "nextcloud" const val Domain = "nextcloud"

View File

@ -2,8 +2,7 @@ package de.mm20.launcher2.files.providers
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.nextcloud.NextcloudApiHelper import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.data.NextcloudFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.min import kotlin.math.min

View File

@ -1,13 +1,16 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.files.providers
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import de.mm20.launcher2.files.OneDriveFileSerializer
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity 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, val fileId: String,
override val label: String, override val label: String,
override val path: String, override val path: String,
@ -42,6 +45,10 @@ data class OneDriveFile(
return context.tryStartActivity(getLaunchIntent(), options) return context.tryStartActivity(getLaunchIntent(), options)
} }
override fun getSerializer(): SearchableSerializer {
return OneDriveFileSerializer()
}
companion object { companion object {
const val Domain = "onedrive" const val Domain = "onedrive"
} }

View File

@ -1,13 +1,16 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.files.providers
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import de.mm20.launcher2.files.OwncloudFileSerializer
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity 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, val fileId: Long,
override val label: String, override val label: String,
override val path: String, override val path: String,
@ -42,6 +45,10 @@ data class OwncloudFile(
return context.tryStartActivity(getLaunchIntent(), options) return context.tryStartActivity(getLaunchIntent(), options)
} }
override fun getSerializer(): SearchableSerializer {
return OwncloudFileSerializer()
}
companion object { companion object {
const val Domain = "owncloud" const val Domain = "owncloud"
} }

View File

@ -2,8 +2,7 @@ package de.mm20.launcher2.files.providers
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.owncloud.OwncloudClient import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.search.data.File import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.data.OwncloudFile
internal class OwncloudFileProvider( internal class OwncloudFileProvider(
private val owncloudClient: OwncloudClient private val owncloudClient: OwncloudClient

View File

@ -45,12 +45,7 @@ dependencies {
implementation(project(":data:calendar")) implementation(project(":data:calendar"))
implementation(project(":core:database")) implementation(project(":core:database"))
implementation(project(":core:preferences")) implementation(project(":core:preferences"))
implementation(project(":data:applications"))
implementation(project(":data:appshortcuts"))
implementation(project(":data:contacts"))
implementation(project(":core:ktx")) implementation(project(":core:ktx"))
implementation(project(":data:files"))
implementation(project(":data:websites"))
implementation(project(":data:wikipedia")) implementation(project(":data:wikipedia"))
implementation(project(":services:badges")) implementation(project(":services:badges"))
implementation(project(":core:crashreporter")) implementation(project(":core:crashreporter"))

View File

@ -6,6 +6,8 @@ import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer import de.mm20.launcher2.icons.TextLayer
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.searchable.TagSerializer
data class Tag( data class Tag(
val tag: String, val tag: String,
@ -33,6 +35,10 @@ data class Tag(
) )
} }
override fun getSerializer(): SearchableSerializer {
return TagSerializer()
}
companion object { companion object {
const val Domain = "tag" const val Domain = "tag"
} }

View File

@ -1,8 +1,12 @@
package de.mm20.launcher2.searchable 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.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val searchableModule = module { val searchableModule = module {
single<SearchableRepository> { SearchableRepositoryImpl(androidContext(), get(), get()) } factory <SavableSearchableRepository> { SavableSearchableRepositoryImpl(androidContext(), get(), get()) }
factory<SearchableDeserializer>(named(Tag.Domain)) { TagDeserializer() }
} }

View File

@ -24,9 +24,13 @@ import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import 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 import java.io.File
interface SearchableRepository { interface SavableSearchableRepository {
fun insert( fun insert(
searchable: SavableSearchable, searchable: SavableSearchable,
@ -114,11 +118,11 @@ interface SearchableRepository {
suspend fun cleanupDatabase(): Int suspend fun cleanupDatabase(): Int
} }
internal class SearchableRepositoryImpl( internal class SavableSearchableRepositoryImpl(
private val context: Context, private val context: Context,
private val database: AppDatabase, private val database: AppDatabase,
private val dataStore: LauncherDataStore private val dataStore: LauncherDataStore
) : SearchableRepository, KoinComponent { ) : SavableSearchableRepository, KoinComponent {
private val scope = CoroutineScope(Job() + Dispatchers.Default) private val scope = CoroutineScope(Job() + Dispatchers.Default)
@ -348,10 +352,17 @@ internal class SearchableRepositoryImpl(
return database.searchableDao().sortByWeight(keys) return database.searchableDao().sortByWeight(keys)
} }
private fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable { private suspend fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable {
val deserializer: SearchableDeserializer = val deserializer: SearchableDeserializer? = try {
getDeserializer(context, entity.type) get(named(entity.type))
val searchable = deserializer.deserialize(entity.serializedSearchable) } 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) if (searchable == null) removeInvalidItem(entity.key)
return SavedSearchable( return SavedSearchable(
key = entity.key, key = entity.key,

View File

@ -15,9 +15,7 @@ data class SavedSearchable(
var weight: Double var weight: Double
) { ) {
fun toDatabaseEntity(): SavedSearchableEntity? { fun toDatabaseEntity(): SavedSearchableEntity? {
val serializer = getSerializer(searchable) val data = searchable?.serialize() ?: return null
val data = searchable?.let { serializer.serialize(it) } ?: return null
return SavedSearchableEntity( return SavedSearchableEntity(
key = key, key = key,

View File

@ -1,114 +1,8 @@
package de.mm20.launcher2.searchable 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.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? { internal fun SavableSearchable.serialize(): String? {
val serializer = getSerializer(this) val serializer = getSerializer()
return serializer.serialize(this) 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()
}

View File

@ -19,7 +19,7 @@ class TagSerializer: SearchableSerializer {
} }
class TagDeserializer: SearchableDeserializer { class TagDeserializer: SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable { override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized) val json = JSONObject(serialized)
return Tag(json.getString("tag")) return Tag(json.getString("tag"))

View File

@ -1,8 +1,13 @@
package de.mm20.launcher2.websites 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.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val websitesModule = module { val websitesModule = module {
single<WebsiteRepository> { WebsiteRepositoryImpl(androidContext()) } single<SearchableRepository<Website>>(named<Website>()) { WebsiteRepository(androidContext()) }
factory<SearchableDeserializer>(named(WebsiteImpl.Domain)) { WebsiteDeserializer() }
} }

View File

@ -1,4 +1,4 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.websites
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -9,19 +9,19 @@ import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import de.mm20.launcher2.icons.* import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.websites.R import de.mm20.launcher2.search.Website
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
data class Website( internal data class WebsiteImpl(
override val label: String, override val label: String,
val url: String, override val url: String,
val description: String, override val description: String,
val image: String, override val imageUrl: String,
val favicon: String, override val faviconUrl: String,
val color: Int, override val color: Int,
override val labelOverride: String? = null, override val labelOverride: String? = null,
) : SavableSearchable { ) : Website {
override val domain: String = Domain override val domain: String = Domain
@ -38,10 +38,10 @@ data class Website(
size: Int, size: Int,
themed: Boolean, themed: Boolean,
): LauncherIcon? { ): LauncherIcon? {
if (favicon.isEmpty()) return null if (faviconUrl.isEmpty()) return null
try { try {
val request = ImageRequest.Builder(context) val request = ImageRequest.Builder(context)
.data(favicon) .data(faviconUrl)
.size(size) .size(size)
.allowHardware(false) .allowHardware(false)
.build() .build()
@ -90,7 +90,9 @@ data class Website(
return context.tryStartActivity(getLaunchIntent(), options) 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) val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra( shareIntent.putExtra(
Intent.EXTRA_TEXT, Intent.EXTRA_TEXT,
@ -100,6 +102,10 @@ data class Website(
context.startActivity(Intent.createChooser(shareIntent, null)) context.startActivity(Intent.createChooser(shareIntent, null))
} }
override fun getSerializer(): SearchableSerializer {
return WebsiteSerializer()
}
companion object { companion object {
const val Domain = "web" const val Domain = "web"
} }

View File

@ -2,8 +2,12 @@ package de.mm20.launcher2.websites
import android.content.Context import android.content.Context
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.compose.runtime.Immutable
import androidx.core.graphics.toColorInt 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.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
@ -20,11 +24,8 @@ import java.net.URISyntaxException
import java.net.URL import java.net.URL
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
interface WebsiteRepository {
fun search(query: String): Flow<Website?>
}
internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository, KoinComponent { internal class WebsiteRepository(val context: Context) : SearchableRepository<Website> {
private val httpClient = OkHttpClient private val httpClient = OkHttpClient
.Builder() .Builder()
@ -33,16 +34,17 @@ internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository,
.writeTimeout(1000, TimeUnit.MILLISECONDS) .writeTimeout(1000, TimeUnit.MILLISECONDS)
.build() .build()
override fun search(query: String): Flow<Website?> = channelFlow { override fun search(query: String): Flow<ImmutableList<Website>> = channelFlow {
send(null) send(persistentListOf())
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
httpClient.dispatcher.cancelAll() httpClient.dispatcher.cancelAll()
} }
if (query.isBlank()) return@channelFlow if (query.isBlank()) return@channelFlow
val website = queryWebsite(query) val website = queryWebsite(query)
send(website) website?.let {
send(persistentListOf(it))
}
} }
private suspend fun queryWebsite(query: String): Website? { 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") doc.head().select("link[href~=.*\\.(ico|png)]").attr("href")
if (favicon.isNotBlank()) favicon = resolveUrl(response.request.url, favicon) if (favicon.isNotBlank()) favicon = resolveUrl(response.request.url, favicon)
if (image.isNotBlank()) image = resolveUrl(response.request.url, image) if (image.isNotBlank()) image = resolveUrl(response.request.url, image)
return@withContext Website( return@withContext WebsiteImpl(
label = title, label = title,
url = url, url = url,
description = description, description = description,
image = image, imageUrl = image,
favicon = favicon, faviconUrl = favicon,
color = color color = color
) )
} catch (e: IOException) { } catch (e: IOException) {

View File

@ -4,18 +4,17 @@ import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Website
import org.json.JSONObject import org.json.JSONObject
class WebsiteSerializer : SearchableSerializer { class WebsiteSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String { override fun serialize(searchable: SavableSearchable): String {
searchable as Website searchable as WebsiteImpl
return jsonObjectOf( return jsonObjectOf(
"label" to searchable.label, "label" to searchable.label,
"url" to searchable.url, "url" to searchable.url,
"description" to searchable.description, "description" to searchable.description,
"image" to searchable.image, "image" to searchable.imageUrl,
"favicon" to searchable.favicon, "favicon" to searchable.faviconUrl,
"color" to searchable.color "color" to searchable.color
).toString() ).toString()
} }
@ -25,12 +24,12 @@ class WebsiteSerializer : SearchableSerializer {
} }
class WebsiteDeserializer: SearchableDeserializer { class WebsiteDeserializer: SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable? { override suspend fun deserialize(serialized: String): SavableSearchable? {
val json = JSONObject(serialized) val json = JSONObject(serialized)
return Website( return WebsiteImpl(
label = json.getString("label"), label = json.getString("label"),
favicon = json.getString("favicon"), faviconUrl = json.getString("favicon"),
image = json.getString("image"), imageUrl = json.getString("image"),
description = json.getString("description"), description = json.getString("description"),
url = json.getString("url"), url = json.getString("url"),
color = json.getInt("color") color = json.getInt("color")

View File

@ -1,8 +1,13 @@
package de.mm20.launcher2.wikipedia 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.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val wikipediaModule = module { val wikipediaModule = module {
single<WikipediaRepository> { WikipediaRepositoryImpl(androidContext(), get()) } single<SearchableRepository<Article>>(named<Article>()) { WikipediaRepository(androidContext(), get()) }
factory<SearchableDeserializer>(named(Wikipedia.Domain)) { WikipediaDeserializer(androidContext()) }
} }

View File

@ -1,34 +1,31 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.wikipedia
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.Article
import de.mm20.launcher2.wikipedia.R import de.mm20.launcher2.search.SearchableSerializer
data class Wikipedia( internal data class Wikipedia(
override val label: String, override val label: String,
val id: Long, val id: Long,
val text: String, override val text: String,
val image: String?, override val imageUrl: String?,
val url: String, override val sourceUrl: String,
override val sourceName: String,
val wikipediaUrl: String, val wikipediaUrl: String,
override val labelOverride: String? = null, override val labelOverride: String? = null,
) : SavableSearchable { ) : Article {
override val domain: String = Domain override val domain: String = Domain
override val preferDetailsOverLaunch: Boolean = false
override fun overrideLabel(label: String): Wikipedia { override fun overrideLabel(label: String): Wikipedia {
return this.copy(labelOverride = label) return this.copy(labelOverride = label)
} }
@ -47,25 +44,30 @@ data class Wikipedia(
} }
private fun getLaunchIntent(): Intent { 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 { override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(), options) 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 text = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
val shareIntent = Intent(Intent.ACTION_SEND) val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra( shareIntent.putExtra(
Intent.EXTRA_TEXT, "${label}\n\n" + Intent.EXTRA_TEXT, "${label}\n\n" +
"${text.substring(0, 200)}\n\n" + "${text.substring(0, 200)}\n\n" +
url sourceUrl
) )
shareIntent.type = "text/plain" shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null)) context.startActivity(Intent.createChooser(shareIntent, null))
} }
override fun getSerializer(): SearchableSerializer {
return WikipediaSerializer()
}
companion object { companion object {
const val Domain = "wikipedia" const val Domain = "wikipedia"
} }

View File

@ -3,7 +3,10 @@ package de.mm20.launcher2.wikipedia
import android.content.Context import android.content.Context
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.preferences.LauncherDataStore 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.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -12,14 +15,11 @@ import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
interface WikipediaRepository {
fun search(query: String, loadImages: Boolean = false): Flow<Wikipedia?>
}
internal class WikipediaRepositoryImpl( internal class WikipediaRepository(
private val context: Context, private val context: Context,
private val dataStore: LauncherDataStore private val dataStore: LauncherDataStore
) : WikipediaRepository, KoinComponent { ) : SearchableRepository<Article> {
private val scope = CoroutineScope(Job() + Dispatchers.Default) private val scope = CoroutineScope(Job() + Dispatchers.Default)
@ -55,8 +55,8 @@ internal class WikipediaRepositoryImpl(
private lateinit var wikipediaService: WikipediaApi private lateinit var wikipediaService: WikipediaApi
override fun search(query: String, loadImages: Boolean): Flow<Wikipedia?> = channelFlow { override fun search(query: String): Flow<ImmutableList<Wikipedia>> = channelFlow {
send(null) send(persistentListOf())
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
httpClient.dispatcher.cancelAll() httpClient.dispatcher.cancelAll()
} }
@ -66,7 +66,10 @@ internal class WikipediaRepositoryImpl(
if (!::wikipediaService.isInitialized) return@channelFlow if (!::wikipediaService.isInitialized) return@channelFlow
if (query.isBlank()) 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? { private suspend fun queryWikipedia(query: String, loadImages: Boolean): Wikipedia? {
@ -92,9 +95,10 @@ internal class WikipediaRepositoryImpl(
label = page.title, label = page.title,
id = page.pageid, id = page.pageid,
text = page.extract, text = page.extract,
image = image, imageUrl = image,
url = page.fullurl, sourceUrl = page.fullurl,
wikipediaUrl = wikipediaUrl wikipediaUrl = wikipediaUrl,
sourceName = context.getString(R.string.wikipedia_source),
) )
} }

View File

@ -4,7 +4,6 @@ import android.content.Context
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Wikipedia
import org.json.JSONObject import org.json.JSONObject
class WikipediaSerializer : SearchableSerializer { class WikipediaSerializer : SearchableSerializer {
@ -14,9 +13,9 @@ class WikipediaSerializer : SearchableSerializer {
json.put("label", searchable.label) json.put("label", searchable.label)
json.put("text", searchable.text) json.put("text", searchable.text)
json.put("id", searchable.id) json.put("id", searchable.id)
json.put("image", searchable.image) json.put("image", searchable.imageUrl)
json.put("wikipedia_url", searchable.wikipediaUrl) json.put("wikipedia_url", searchable.wikipediaUrl)
json.put("url", searchable.url) json.put("url", searchable.sourceUrl)
return json.toString() return json.toString()
} }
@ -25,7 +24,7 @@ class WikipediaSerializer : SearchableSerializer {
} }
class WikipediaDeserializer(val context: Context) : SearchableDeserializer { class WikipediaDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): SavableSearchable? { override suspend fun deserialize(serialized: String): SavableSearchable? {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val wikipediaUrl = json.optString("wikipedia_url").takeIf { !it.isNullOrBlank() } ?: return null val wikipediaUrl = json.optString("wikipedia_url").takeIf { !it.isNullOrBlank() } ?: return null
val id = json.getLong("id") val id = json.getLong("id")
@ -33,9 +32,10 @@ class WikipediaDeserializer(val context: Context) : SearchableDeserializer {
label = json.getString("label"), label = json.getString("label"),
text = json.getString("text"), text = json.getString("text"),
id = id, id = id,
image = json.optString("image"), imageUrl = json.optString("image"),
url = json.optString("url").takeIf { !it.isNullOrBlank() } ?: "${wikipediaUrl.padEnd(1, '/')}wiki?curid=$id", sourceUrl = json.optString("url").takeIf { !it.isNullOrBlank() } ?: "${wikipediaUrl.padEnd(1, '/')}wiki?curid=$id",
wikipediaUrl = wikipediaUrl wikipediaUrl = wikipediaUrl,
sourceName = context.getString(R.string.wikipedia_source),
) )
} }
} }

View File

@ -15,6 +15,7 @@ digraph {
":core:ktx" [fillcolor="#94c1ff"]; ":core:ktx" [fillcolor="#94c1ff"];
":core:permissions" [fillcolor="#94c1ff"]; ":core:permissions" [fillcolor="#94c1ff"];
":core:preferences" [fillcolor="#94c1ff"]; ":core:preferences" [fillcolor="#94c1ff"];
":core:shared" [fillcolor="#94c1ff"];
":data:applications" [fillcolor="#fff694"]; ":data:applications" [fillcolor="#fff694"];
":data:appshortcuts" [fillcolor="#fff694"]; ":data:appshortcuts" [fillcolor="#fff694"];
":data:calculator" [fillcolor="#fff694"]; ":data:calculator" [fillcolor="#fff694"];
@ -22,10 +23,11 @@ digraph {
":data:contacts" [fillcolor="#fff694"]; ":data:contacts" [fillcolor="#fff694"];
":data:currencies" [fillcolor="#fff694"]; ":data:currencies" [fillcolor="#fff694"];
":data:customattrs" [fillcolor="#fff694"]; ":data:customattrs" [fillcolor="#fff694"];
":data:favorites" [fillcolor="#fff694"];
":data:files" [fillcolor="#fff694"]; ":data:files" [fillcolor="#fff694"];
":data:notifications" [fillcolor="#fff694"]; ":data:notifications" [fillcolor="#fff694"];
":data:search-actions" [fillcolor="#fff694"]; ":data:search-actions" [fillcolor="#fff694"];
":data:searchable" [fillcolor="#fff694"];
":data:themes" [fillcolor="#fff694"];
":data:unitconverter" [fillcolor="#fff694"]; ":data:unitconverter" [fillcolor="#fff694"];
":data:weather" [fillcolor="#fff694"]; ":data:weather" [fillcolor="#fff694"];
":data:websites" [fillcolor="#fff694"]; ":data:websites" [fillcolor="#fff694"];
@ -37,9 +39,11 @@ digraph {
":libs:nextcloud" [fillcolor="#ad94ff"]; ":libs:nextcloud" [fillcolor="#ad94ff"];
":libs:owncloud" [fillcolor="#ad94ff"]; ":libs:owncloud" [fillcolor="#ad94ff"];
":libs:webdav" [fillcolor="#ad94ff"]; ":libs:webdav" [fillcolor="#ad94ff"];
":plugins:sdk" [];
":services:accounts" [fillcolor="#ff9498"]; ":services:accounts" [fillcolor="#ff9498"];
":services:backup" [fillcolor="#ff9498"]; ":services:backup" [fillcolor="#ff9498"];
":services:badges" [fillcolor="#ff9498"]; ":services:badges" [fillcolor="#ff9498"];
":services:favorites" [fillcolor="#ff9498"];
":services:global-actions" [fillcolor="#ff9498"]; ":services:global-actions" [fillcolor="#ff9498"];
":services:icons" [fillcolor="#ff9498"]; ":services:icons" [fillcolor="#ff9498"];
":services:music" [fillcolor="#ff9498"]; ":services:music" [fillcolor="#ff9498"];
@ -64,13 +68,13 @@ digraph {
":app:app" -> ":core:crashreporter" [style=dotted] ":app:app" -> ":core:crashreporter" [style=dotted]
":app:app" -> ":data:currencies" [style=dotted] ":app:app" -> ":data:currencies" [style=dotted]
":app:app" -> ":data:customattrs" [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" -> ":data:files" [style=dotted]
":app:app" -> ":libs:g-services" [style=dotted] ":app:app" -> ":libs:g-services" [style=dotted]
":app:app" -> ":core:i18n" [style=dotted] ":app:app" -> ":core:i18n" [style=dotted]
":app:app" -> ":services:icons" [style=dotted] ":app:app" -> ":services:icons" [style=dotted]
":app:app" -> ":core:ktx" [style=dotted] ":app:app" -> ":core:ktx" [style=dotted]
":app:app" -> ":libs:ms-services" [style=dotted]
":app:app" -> ":services:music" [style=dotted] ":app:app" -> ":services:music" [style=dotted]
":app:app" -> ":libs:nextcloud" [style=dotted] ":app:app" -> ":libs:nextcloud" [style=dotted]
":app:app" -> ":data:notifications" [style=dotted] ":app:app" -> ":data:notifications" [style=dotted]
@ -89,6 +93,7 @@ digraph {
":app:app" -> ":data:search-actions" [style=dotted] ":app:app" -> ":data:search-actions" [style=dotted]
":app:app" -> ":services:global-actions" [style=dotted] ":app:app" -> ":services:global-actions" [style=dotted]
":app:app" -> ":services:widgets" [style=dotted] ":app:app" -> ":services:widgets" [style=dotted]
":app:app" -> ":services:favorites" [style=dotted]
":app:ui" -> ":app:ui" ":app:ui" -> ":app:ui"
":app:ui" -> ":libs:material-color-utilities" [style=dotted] ":app:ui" -> ":libs:material-color-utilities" [style=dotted]
":app:ui" -> ":core:base" [style=dotted] ":app:ui" -> ":core:base" [style=dotted]
@ -107,7 +112,8 @@ digraph {
":app:ui" -> ":data:calculator" [style=dotted] ":app:ui" -> ":data:calculator" [style=dotted]
":app:ui" -> ":data:files" [style=dotted] ":app:ui" -> ":data:files" [style=dotted]
":app:ui" -> ":data:widgets" [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" -> ":data:wikipedia" [style=dotted]
":app:ui" -> ":services:badges" [style=dotted] ":app:ui" -> ":services:badges" [style=dotted]
":app:ui" -> ":core:crashreporter" [style=dotted] ":app:ui" -> ":core:crashreporter" [style=dotted]
@ -118,112 +124,50 @@ digraph {
":app:ui" -> ":data:unitconverter" [style=dotted] ":app:ui" -> ":data:unitconverter" [style=dotted]
":app:ui" -> ":libs:nextcloud" [style=dotted] ":app:ui" -> ":libs:nextcloud" [style=dotted]
":app:ui" -> ":libs:g-services" [style=dotted] ":app:ui" -> ":libs:g-services" [style=dotted]
":app:ui" -> ":libs:ms-services" [style=dotted]
":app:ui" -> ":libs:owncloud" [style=dotted] ":app:ui" -> ":libs:owncloud" [style=dotted]
":app:ui" -> ":services:accounts" [style=dotted] ":app:ui" -> ":services:accounts" [style=dotted]
":app:ui" -> ":services:backup" [style=dotted] ":app:ui" -> ":services:backup" [style=dotted]
":app:ui" -> ":data:search-actions" [style=dotted] ":app:ui" -> ":data:search-actions" [style=dotted]
":app:ui" -> ":services:global-actions" [style=dotted] ":app:ui" -> ":services:global-actions" [style=dotted]
":app:ui" -> ":services:widgets" [style=dotted] ":app:ui" -> ":services:widgets" [style=dotted]
":core:base" -> ":core:base" ":app:ui" -> ":services:favorites" [style=dotted]
":core:base" -> ":core:ktx" [style=dotted] ":core:shared" -> ":core:shared"
":core:base" -> ":core:i18n" [style=dotted]
":core:compat" -> ":core:compat"
":core:crashreporter" -> ":core:crashreporter"
":core:crashreporter" -> ":core:base" [style=dotted]
":core:database" -> ":core:database" ":core:database" -> ":core:database"
":core:database" -> ":core:i18n" [style=dotted] ":core:database" -> ":core:i18n" [style=dotted]
":core:database" -> ":core:ktx" [style=dotted] ":core:database" -> ":core:ktx" [style=dotted]
":core:i18n" -> ":core:i18n" ":core:database" -> ":core:preferences" [style=dotted]
":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:preferences" -> ":core:preferences" ":core:preferences" -> ":core:preferences"
":core:preferences" -> ":core:ktx" [style=dotted] ":core:preferences" -> ":core:ktx" [style=dotted]
":core:preferences" -> ":core:i18n" [style=dotted] ":core:preferences" -> ":core:i18n" [style=dotted]
":core:preferences" -> ":core:base" [style=dotted] ":core:preferences" -> ":core:base" [style=dotted]
":core:preferences" -> ":core:crashreporter" [style=dotted] ":core:preferences" -> ":core:crashreporter" [style=dotted]
":core:preferences" -> ":libs:material-color-utilities" [style=dotted] ":core:preferences" -> ":libs:material-color-utilities" [style=dotted]
":data:applications" -> ":data:applications" ":core:permissions" -> ":core:permissions"
":data:applications" -> ":core:base" [style=dotted] ":core:permissions" -> ":core:ktx" [style=dotted]
":data:applications" -> ":core:ktx" [style=dotted] ":core:permissions" -> ":core:base" [style=dotted]
":data:applications" -> ":core:compat" [style=dotted] ":core:permissions" -> ":core:crashreporter" [style=dotted]
":data:appshortcuts" -> ":data:appshortcuts" ":core:compat" -> ":core:compat"
":data:appshortcuts" -> ":data:applications" [style=dotted] ":core:crashreporter" -> ":core:crashreporter"
":data:appshortcuts" -> ":core:permissions" [style=dotted] ":core:crashreporter" -> ":core:base" [style=dotted]
":data:appshortcuts" -> ":core:base" [style=dotted] ":core:i18n" -> ":core:i18n"
":data:appshortcuts" -> ":core:ktx" [style=dotted] ":core:ktx" -> ":core:ktx"
":data:calculator" -> ":data:calculator" ":core:base" -> ":core:base"
":data:calculator" -> ":core:base" [style=dotted] ":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" -> ":data:calendar"
":data:calendar" -> ":core:ktx" [style=dotted] ":data:calendar" -> ":core:ktx" [style=dotted]
":data:calendar" -> ":core:base" [style=dotted] ":data:calendar" -> ":core:base" [style=dotted]
":data:calendar" -> ":core:permissions" [style=dotted] ":data:calendar" -> ":core:permissions" [style=dotted]
":data:calendar" -> ":libs:material-color-utilities" [style=dotted] ":data:calendar" -> ":libs:material-color-utilities" [style=dotted]
":data:contacts" -> ":data:contacts" ":data:calculator" -> ":data:calculator"
":data:contacts" -> ":core:ktx" [style=dotted] ":data:calculator" -> ":core:base" [style=dotted]
":data:contacts" -> ":core:base" [style=dotted] ":data:appshortcuts" -> ":data:appshortcuts"
":data:contacts" -> ":core:permissions" [style=dotted] ":data:appshortcuts" -> ":data:applications" [style=dotted]
":data:currencies" -> ":data:currencies" ":data:appshortcuts" -> ":core:permissions" [style=dotted]
":data:currencies" -> ":core:ktx" [style=dotted] ":data:appshortcuts" -> ":core:base" [style=dotted]
":data:currencies" -> ":core:i18n" [style=dotted] ":data:appshortcuts" -> ":core:ktx" [style=dotted]
":data:currencies" -> ":core:database" [style=dotted] ":data:appshortcuts" -> ":core:crashreporter" [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:widgets" -> ":data:widgets" ":data:widgets" -> ":data:widgets"
":data:widgets" -> ":data:weather" [style=dotted] ":data:widgets" -> ":data:weather" [style=dotted]
":data:widgets" -> ":data:calendar" [style=dotted] ":data:widgets" -> ":data:calendar" [style=dotted]
@ -233,40 +177,79 @@ digraph {
":data:widgets" -> ":core:preferences" [style=dotted] ":data:widgets" -> ":core:preferences" [style=dotted]
":data:widgets" -> ":core:database" [style=dotted] ":data:widgets" -> ":core:database" [style=dotted]
":data:widgets" -> ":core:crashreporter" [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" -> ":data:wikipedia"
":data:wikipedia" -> ":core:preferences" [style=dotted] ":data:wikipedia" -> ":core:preferences" [style=dotted]
":data:wikipedia" -> ":core:base" [style=dotted] ":data:wikipedia" -> ":core:base" [style=dotted]
":data:wikipedia" -> ":core:ktx" [style=dotted] ":data:wikipedia" -> ":core:ktx" [style=dotted]
":data:wikipedia" -> ":core:crashreporter" [style=dotted] ":data:wikipedia" -> ":core:crashreporter" [style=dotted]
":libs:g-services" -> ":libs:g-services" ":data:contacts" -> ":data:contacts"
":libs:g-services" -> ":core:i18n" [style=dotted] ":data:contacts" -> ":core:ktx" [style=dotted]
":libs:g-services" -> ":core:crashreporter" [style=dotted] ":data:contacts" -> ":core:base" [style=dotted]
":libs:material-color-utilities" -> ":libs:material-color-utilities" ":data:contacts" -> ":core:permissions" [style=dotted]
":libs:ms-services" -> ":libs:ms-services" ":data:notifications" -> ":data:notifications"
":libs:ms-services" -> ":core:crashreporter" [style=dotted] ":data:notifications" -> ":core:permissions" [style=dotted]
":libs:nextcloud" -> ":libs:webdav" ":data:applications" -> ":data:applications"
":libs:nextcloud" -> ":libs:nextcloud" ":data:applications" -> ":core:base" [style=dotted]
":libs:nextcloud" -> ":core:i18n" [style=dotted] ":data:applications" -> ":core:ktx" [style=dotted]
":libs:owncloud" -> ":libs:webdav" ":data:applications" -> ":core:compat" [style=dotted]
":libs:owncloud" -> ":libs:owncloud" ":data:currencies" -> ":data:currencies"
":libs:owncloud" -> ":core:crashreporter" [style=dotted] ":data:currencies" -> ":core:ktx" [style=dotted]
":libs:owncloud" -> ":core:ktx" [style=dotted] ":data:currencies" -> ":core:i18n" [style=dotted]
":libs:owncloud" -> ":core:i18n" [style=dotted] ":data:currencies" -> ":core:database" [style=dotted]
":libs:webdav" -> ":libs:webdav" ":data:currencies" -> ":core:crashreporter" [style=dotted]
":libs:webdav" -> ":core:crashreporter" [style=dotted] ":plugins:sdk" -> ":plugins:sdk"
":libs:webdav" -> ":core:ktx" [style=dotted] ":plugins:sdk" -> ":core:shared" [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]
":services:badges" -> ":services:badges" ":services:badges" -> ":services:badges"
":services:badges" -> ":core:ktx" [style=dotted] ":services:badges" -> ":core:ktx" [style=dotted]
":services:badges" -> ":data:applications" [style=dotted] ":services:badges" -> ":data:applications" [style=dotted]
@ -275,11 +258,36 @@ digraph {
":services:badges" -> ":core:preferences" [style=dotted] ":services:badges" -> ":core:preferences" [style=dotted]
":services:badges" -> ":core:base" [style=dotted] ":services:badges" -> ":core:base" [style=dotted]
":services:badges" -> ":data:files" [style=dotted] ":services:badges" -> ":data:files" [style=dotted]
":services:global-actions" -> ":services:global-actions" ":services:favorites" -> ":services:favorites"
":services:global-actions" -> ":core:preferences" [style=dotted] ":services:favorites" -> ":core:base" [style=dotted]
":services:global-actions" -> ":core:base" [style=dotted] ":services:favorites" -> ":core:i18n" [style=dotted]
":services:global-actions" -> ":core:i18n" [style=dotted] ":services:favorites" -> ":data:searchable" [style=dotted]
":services:global-actions" -> ":core:permissions" [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" -> ":data:customattrs"
":services:icons" -> ":services:icons" ":services:icons" -> ":services:icons"
":services:icons" -> ":core:database" [style=dotted] ":services:icons" -> ":core:database" [style=dotted]
@ -288,37 +296,37 @@ digraph {
":services:icons" -> ":core:base" [style=dotted] ":services:icons" -> ":core:base" [style=dotted]
":services:icons" -> ":data:applications" [style=dotted] ":services:icons" -> ":data:applications" [style=dotted]
":services:icons" -> ":core:crashreporter" [style=dotted] ":services:icons" -> ":core:crashreporter" [style=dotted]
":services:music" -> ":services:music" ":services:widgets" -> ":services:widgets"
":services:music" -> ":core:ktx" [style=dotted] ":services:widgets" -> ":core:base" [style=dotted]
":services:music" -> ":core:preferences" [style=dotted] ":services:widgets" -> ":core:i18n" [style=dotted]
":services:music" -> ":data:notifications" [style=dotted] ":services:widgets" -> ":data:widgets" [style=dotted]
":services:music" -> ":core:crashreporter" [style=dotted] ":services:global-actions" -> ":services:global-actions"
":services:search" -> ":services:search" ":services:global-actions" -> ":core:preferences" [style=dotted]
":services:search" -> ":data:applications" [style=dotted] ":services:global-actions" -> ":core:base" [style=dotted]
":services:search" -> ":data:appshortcuts" [style=dotted] ":services:global-actions" -> ":core:i18n" [style=dotted]
":services:search" -> ":data:calculator" [style=dotted] ":services:global-actions" -> ":core:permissions" [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:tags" -> ":services:tags" ":services:tags" -> ":services:tags"
":services:tags" -> ":core:preferences" [style=dotted] ":services:tags" -> ":core:preferences" [style=dotted]
":services:tags" -> ":core:base" [style=dotted] ":services:tags" -> ":core:base" [style=dotted]
":services:tags" -> ":core:ktx" [style=dotted] ":services:tags" -> ":core:ktx" [style=dotted]
":services:tags" -> ":core:crashreporter" [style=dotted] ":services:tags" -> ":core:crashreporter" [style=dotted]
":services:tags" -> ":data:customattrs" [style=dotted] ":services:tags" -> ":data:customattrs" [style=dotted]
":services:tags" -> ":data:favorites" [style=dotted] ":services:tags" -> ":data:searchable" [style=dotted]
":services:widgets" -> ":services:widgets" ":libs:nextcloud" -> ":libs:webdav"
":services:widgets" -> ":core:base" [style=dotted] ":libs:nextcloud" -> ":libs:nextcloud"
":services:widgets" -> ":core:i18n" [style=dotted] ":libs:nextcloud" -> ":core:i18n" [style=dotted]
":services:widgets" -> ":data:widgets" [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]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.searchable.SearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.export import de.mm20.launcher2.preferences.export
import de.mm20.launcher2.preferences.import import de.mm20.launcher2.preferences.import
@ -22,7 +22,7 @@ import java.util.zip.ZipOutputStream
class BackupManager( class BackupManager(
private val context: Context, private val context: Context,
private val dataStore: LauncherDataStore, private val dataStore: LauncherDataStore,
private val searchableRepository: SearchableRepository, private val searchableRepository: SavableSearchableRepository,
private val widgetRepository: WidgetRepository, private val widgetRepository: WidgetRepository,
private val searchActionRepository: SearchActionRepository, private val searchActionRepository: SearchActionRepository,
private val customAttrsRepository: CustomAttributesRepository, private val customAttrsRepository: CustomAttributesRepository,

View File

@ -4,10 +4,8 @@ import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.graphics.BadgeDrawable import de.mm20.launcher2.graphics.BadgeDrawable
import de.mm20.launcher2.search.data.LauncherShortcut import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.data.LegacyShortcut
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.UnavailableShortcut
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
@ -17,53 +15,37 @@ class AppShortcutBadgeProvider(
private val context: Context private val context: Context
) : BadgeProvider { ) : BadgeProvider {
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow { override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
if (searchable is LauncherShortcut) { if (searchable is AppShortcut) {
val componentName = searchable.launcherShortcut.activity val componentName = searchable.componentName
if (componentName == null) { 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) send(null)
return@channelFlow 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 { } else {
send(null) send(null)
} }

View File

@ -1,7 +1,7 @@
package de.mm20.launcher2.badges.providers package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge 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 de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow

View File

@ -3,7 +3,7 @@ package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.notifications.NotificationRepository import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.search.Searchable 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.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -15,8 +15,8 @@ class NotificationBadgeProvider : BadgeProvider, KoinComponent {
private val notificationRepository: NotificationRepository by inject() private val notificationRepository: NotificationRepository by inject()
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow { override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
if (searchable is LauncherApp) { if (searchable is Application) {
val packageName = searchable.`package` val packageName = searchable.componentName.packageName
notificationRepository.notifications.map { notificationRepository.notifications.map {
it.filter { it.packageName == packageName } it.filter { it.packageName == packageName }
}.collectLatest { }.collectLatest {

View File

@ -1,33 +1,18 @@
package de.mm20.launcher2.badges.providers package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.R import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SuspendedAppsBadgeProvider : BadgeProvider, KoinComponent { class SuspendedAppsBadgeProvider : BadgeProvider, KoinComponent {
private val appRepository: AppRepository by inject()
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow { override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
if (searchable is LauncherApp) { if (searchable is Application && searchable.isSuspended) {
val packageName = searchable.`package` send(Badge(iconRes = R.drawable.ic_badge_suspended))
appRepository.getSuspendedPackages().collectLatest {
if (it.contains(packageName)) {
send(
Badge(
iconRes = R.drawable.ic_badge_suspended
)
)
} else {
send(null)
}
}
} else { } else {
send(null) send(null)
} }

View File

@ -2,15 +2,16 @@ package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.R import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.AppProfile
import de.mm20.launcher2.search.data.LauncherShortcut import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
class WorkProfileBadgeProvider : BadgeProvider { class WorkProfileBadgeProvider : BadgeProvider {
override fun getBadge(searchable: Searchable): Flow<Badge?> = flow { override fun getBadge(searchable: Searchable): Flow<Badge?> = 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( emit(
Badge( Badge(
iconRes = R.drawable.ic_badge_workprofile iconRes = R.drawable.ic_badge_workprofile

View File

@ -1,11 +1,11 @@
package de.mm20.launcher2.services.favorites package de.mm20.launcher2.services.favorites
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.searchable.SearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class FavoritesService( class FavoritesService(
val searchableRepository: SearchableRepository, val searchableRepository: SavableSearchableRepository,
) { ) {

Some files were not shown because too many files have changed in this diff Show More