Annual refactor
This commit is contained in:
parent
4f64652aae
commit
957358c79a
@ -3,8 +3,7 @@ package de.mm20.launcher2.activity
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.appshortcuts.AppShortcut
|
||||
import de.mm20.launcher2.services.favorites.FavoritesService
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
@ -14,7 +13,7 @@ class AddItemActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val shortcut = AppShortcut.fromPinRequestIntent(this, intent)
|
||||
val shortcut = AppShortcut(this, intent)
|
||||
if (shortcut != null) {
|
||||
favoritesService.pinItem(shortcut)
|
||||
} else {
|
||||
|
||||
@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
||||
import de.mm20.launcher2.data.customattrs.utils.withCustomLabels
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.Tag
|
||||
|
||||
@ -31,8 +31,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import coil.compose.AsyncImage
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.abs
|
||||
@ -107,23 +107,12 @@ fun rememberSplashScreenData(searchable: SavableSearchable?): SplashScreenData {
|
||||
|
||||
LaunchedEffect(searchable) {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (searchable is LauncherApp) {
|
||||
val activityInfo = if (isAtLeastApiLevel(31)) {
|
||||
searchable.launcherActivityInfo.activityInfo
|
||||
} else {
|
||||
try {
|
||||
context.packageManager.getActivityInfo(
|
||||
searchable.launcherActivityInfo.componentName,
|
||||
0
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
} ?: return@withContext
|
||||
if (searchable is Application) {
|
||||
val activityInfo = searchable.getActivityInfo(context) ?: return@withContext
|
||||
val themeRes = activityInfo.themeResource
|
||||
val ctx = try {
|
||||
context.createPackageContext(
|
||||
searchable.`package`,
|
||||
searchable.componentName.packageName,
|
||||
Context.CONTEXT_IGNORE_SECURITY
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
|
||||
@ -8,7 +8,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||
import de.mm20.launcher2.globalactions.GlobalActionsService
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
@ -35,7 +35,7 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
private val globalActionsService: GlobalActionsService by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val searchableRepository: SearchableRepository by inject()
|
||||
private val searchableRepository: SavableSearchableRepository by inject()
|
||||
|
||||
private var isSystemInDarkMode = MutableStateFlow(false)
|
||||
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
package de.mm20.launcher2.ui.launcher.search
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@ -18,29 +15,22 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.ExpandMore
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.Tag
|
||||
import androidx.compose.material.icons.rounded.Work
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@ -61,7 +51,7 @@ import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
|
||||
import de.mm20.launcher2.ui.launcher.search.favorites.SearchFavoritesVM
|
||||
import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterItem
|
||||
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItem
|
||||
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaItem
|
||||
import de.mm20.launcher2.ui.launcher.search.wikipedia.ArticleItem
|
||||
import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||
@ -99,7 +89,7 @@ fun SearchColumn(
|
||||
val events by viewModel.calendarResults
|
||||
val unitConverter by viewModel.unitConverterResults
|
||||
val calculator by viewModel.calculatorResults
|
||||
val wikipedia by viewModel.wikipediaResults
|
||||
val wikipedia by viewModel.articleResults
|
||||
val website by viewModel.websiteResults
|
||||
val hiddenResults by viewModel.hiddenResults
|
||||
|
||||
@ -307,7 +297,7 @@ fun SearchColumn(
|
||||
)
|
||||
for (wiki in wikipedia) {
|
||||
SingleResult(highlight = bestMatch == wiki) {
|
||||
WikipediaItem(wikipedia = wiki)
|
||||
ArticleItem(article = wiki)
|
||||
}
|
||||
}
|
||||
for (ws in website) {
|
||||
|
||||
@ -5,23 +5,24 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchResultOrderingSettings.Ordering
|
||||
import de.mm20.launcher2.search.AppProfile
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchService
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.Article
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.Website
|
||||
import de.mm20.launcher2.search.data.Calculator
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.data.UnitConverter
|
||||
import de.mm20.launcher2.search.data.Website
|
||||
import de.mm20.launcher2.search.data.Wikipedia
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.services.favorites.FavoritesService
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@ -43,7 +44,7 @@ import org.koin.core.component.inject
|
||||
class SearchVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val favoritesService: FavoritesService by inject()
|
||||
private val searchableRepository: SearchableRepository by inject()
|
||||
private val searchableRepository: SavableSearchableRepository by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
@ -55,13 +56,13 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
val searchQuery = mutableStateOf("")
|
||||
val isSearchEmpty = mutableStateOf(true)
|
||||
|
||||
val appResults = mutableStateOf<List<LauncherApp>>(emptyList())
|
||||
val workAppResults = mutableStateOf<List<LauncherApp>>(emptyList())
|
||||
val appResults = mutableStateOf<List<Application>>(emptyList())
|
||||
val workAppResults = mutableStateOf<List<Application>>(emptyList())
|
||||
val appShortcutResults = mutableStateOf<List<AppShortcut>>(emptyList())
|
||||
val fileResults = mutableStateOf<List<File>>(emptyList())
|
||||
val contactResults = mutableStateOf<List<Contact>>(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 calculatorResults = mutableStateOf<List<Calculator>>(emptyList())
|
||||
val unitConverterResults = mutableStateOf<List<UnitConverter>>(emptyList())
|
||||
@ -185,15 +186,15 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
|
||||
hiddenItemKeys.collectLatest { hiddenKeys ->
|
||||
val hidden = mutableListOf<SavableSearchable>()
|
||||
val apps = mutableListOf<LauncherApp>()
|
||||
val workApps = mutableListOf<LauncherApp>()
|
||||
val apps = mutableListOf<Application>()
|
||||
val workApps = mutableListOf<Application>()
|
||||
val shortcuts = mutableListOf<AppShortcut>()
|
||||
val files = mutableListOf<File>()
|
||||
val contacts = mutableListOf<Contact>()
|
||||
val events = mutableListOf<CalendarEvent>()
|
||||
val unitConv = mutableListOf<UnitConverter>()
|
||||
val calc = mutableListOf<Calculator>()
|
||||
val wikipedia = mutableListOf<Wikipedia>()
|
||||
val articles = mutableListOf<Article>()
|
||||
val website = mutableListOf<Website>()
|
||||
val actions = mutableListOf<SearchAction>()
|
||||
for (r in resultsList) {
|
||||
@ -202,8 +203,8 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
hidden.add(r)
|
||||
}
|
||||
|
||||
r is LauncherApp && !r.isMainProfile -> workApps.add(r)
|
||||
r is LauncherApp -> apps.add(r)
|
||||
r is Application && r.profile == AppProfile.Work -> workApps.add(r)
|
||||
r is Application -> apps.add(r)
|
||||
r is AppShortcut -> shortcuts.add(r)
|
||||
r is File -> files.add(r)
|
||||
r is Contact -> contacts.add(r)
|
||||
@ -211,7 +212,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
r is UnitConverter -> unitConv.add(r)
|
||||
r is Calculator -> calc.add(r)
|
||||
r is Website -> website.add(r)
|
||||
r is Wikipedia -> wikipedia.add(r)
|
||||
r is Article -> articles.add(r)
|
||||
r is SearchAction -> actions.add(r)
|
||||
}
|
||||
}
|
||||
@ -225,7 +226,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
calc,
|
||||
events,
|
||||
contacts,
|
||||
wikipedia,
|
||||
articles,
|
||||
website,
|
||||
files,
|
||||
actions
|
||||
@ -238,7 +239,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
fileResults.value = files
|
||||
contactResults.value = contacts
|
||||
calendarResults.value = events
|
||||
wikipediaResults.value = wikipedia
|
||||
articleResults.value = articles
|
||||
websiteResults.value = website
|
||||
calculatorResults.value = calc
|
||||
unitConverterResults.value = unitConv
|
||||
|
||||
@ -66,7 +66,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||
@ -82,12 +82,11 @@ import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||
import de.mm20.launcher2.ui.modifier.scale
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.pow
|
||||
|
||||
@Composable
|
||||
fun AppItem(
|
||||
modifier: Modifier = Modifier,
|
||||
app: LauncherApp,
|
||||
app: Application,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${app.key}")
|
||||
@ -127,7 +126,7 @@ fun AppItem(
|
||||
}
|
||||
|
||||
|
||||
app.version?.let {
|
||||
app.versionName?.let {
|
||||
Text(
|
||||
text = stringResource(R.string.app_info_version, it),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@ -137,7 +136,7 @@ fun AppItem(
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = app.`package`,
|
||||
text = app.componentName.packageName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(top = 1.dp),
|
||||
maxLines = 1,
|
||||
@ -210,20 +209,21 @@ fun AppItem(
|
||||
|
||||
for (shortcut in shortcuts) {
|
||||
val title =
|
||||
shortcut.launcherShortcut.shortLabel
|
||||
?: shortcut.launcherShortcut.longLabel
|
||||
?: continue
|
||||
shortcut.labelOverride ?: shortcut.label
|
||||
val isPinned by remember(shortcut) { viewModel.isShortcutPinned(shortcut) }.collectAsState(
|
||||
false
|
||||
)
|
||||
|
||||
val icon =
|
||||
val iconSizePx = InputChipDefaults.AvatarSize.toPixels()
|
||||
|
||||
val icon by
|
||||
remember {
|
||||
viewModel.getShortcutIcon(
|
||||
context,
|
||||
shortcut.launcherShortcut
|
||||
shortcut,
|
||||
iconSizePx.toInt()
|
||||
)
|
||||
}
|
||||
}.collectAsState(null)
|
||||
|
||||
InputChip(
|
||||
modifier = Modifier.width(IntrinsicSize.Max),
|
||||
@ -233,19 +233,17 @@ fun AppItem(
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
title.toString(),
|
||||
title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
},
|
||||
avatar = {
|
||||
AsyncImage(
|
||||
model = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(InputChipDefaults.AvatarSize),
|
||||
ShapedLauncherIcon(
|
||||
size = InputChipDefaults.AvatarSize,
|
||||
icon = { icon },
|
||||
shape = CircleShape,
|
||||
)
|
||||
},
|
||||
trailingIcon = if (LocalFavoritesEnabled.current) {
|
||||
@ -311,7 +309,7 @@ fun AppItem(
|
||||
label = stringResource(R.string.menu_app_info),
|
||||
icon = Icons.Rounded.Info
|
||||
) {
|
||||
app.openAppInfo(context)
|
||||
app.openAppDetails(context)
|
||||
})
|
||||
|
||||
toolbarActions.add(
|
||||
@ -429,7 +427,7 @@ fun AppItem(
|
||||
|
||||
@Composable
|
||||
fun AppItemGridPopup(
|
||||
app: LauncherApp,
|
||||
app: Application,
|
||||
show: MutableTransitionState<Boolean>,
|
||||
animationProgress: Float,
|
||||
origin: Rect,
|
||||
@ -454,7 +452,7 @@ fun AppItemGridPopup(
|
||||
transformOrigin = TransformOrigin(1f, 0f)
|
||||
)
|
||||
.offset(
|
||||
x = lerp(16.dp, 0.dp, animationProgress),
|
||||
x = lerp(16.dp, 0.dp, animationProgress),
|
||||
y = lerp(-16.dp, 0.dp, animationProgress)
|
||||
),
|
||||
app = app,
|
||||
|
||||
@ -41,13 +41,14 @@ import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.roundToIntRect
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
@ -83,12 +84,13 @@ fun CalendarItem(
|
||||
val snackbarHostState = LocalSnackbarHostState.current
|
||||
|
||||
val darkMode = LocalDarkTheme.current
|
||||
val secondaryColor = MaterialTheme.colorScheme.secondary
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.drawBehind {
|
||||
val color = TonalPalette
|
||||
.fromInt(calendar.color)
|
||||
.fromInt(calendar.color ?: secondaryColor.toArgb())
|
||||
.tone(
|
||||
if (darkMode) 80 else 40
|
||||
)
|
||||
@ -151,7 +153,7 @@ fun CalendarItem(
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
if (calendar.description.isNotBlank()) {
|
||||
if (calendar.description != null) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
@ -163,7 +165,7 @@ fun CalendarItem(
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = calendar.description,
|
||||
text = calendar.description!!,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
@ -185,7 +187,7 @@ fun CalendarItem(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (calendar.location.isNotBlank()) {
|
||||
if (calendar.location != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
@ -200,7 +202,7 @@ fun CalendarItem(
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = calendar.location,
|
||||
text = calendar.location!!,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
@ -320,8 +322,7 @@ fun CalendarItemGridPopup(
|
||||
) {
|
||||
CalendarItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color(calendar.color).copy(alpha = 1f - animationProgress)),
|
||||
.fillMaxWidth(),
|
||||
calendar = calendar,
|
||||
showDetails = true,
|
||||
onBack = onDismiss
|
||||
|
||||
@ -4,27 +4,22 @@ import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.service.notification.StatusBarNotification
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
|
||||
import de.mm20.launcher2.badges.BadgeService
|
||||
import de.mm20.launcher2.files.FileRepository
|
||||
import de.mm20.launcher2.icons.IconService
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.notifications.Notification
|
||||
import de.mm20.launcher2.notifications.NotificationRepository
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.services.favorites.FavoritesService
|
||||
import de.mm20.launcher2.services.tags.TagsService
|
||||
import de.mm20.launcher2.ui.launcher.search.ListItemViewModel
|
||||
@ -33,9 +28,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
@ -46,7 +43,6 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
||||
private val tagsService: TagsService by inject()
|
||||
private val notificationRepository: NotificationRepository by inject()
|
||||
private val appShortcutRepository: AppShortcutRepository by inject()
|
||||
private val fileRepository: FileRepository by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
|
||||
private val searchable = MutableStateFlow<SavableSearchable?>(null)
|
||||
@ -93,13 +89,19 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
||||
}
|
||||
|
||||
val notifications = searchable.flatMapLatest { searchable ->
|
||||
if (searchable !is LauncherApp) emptyFlow()
|
||||
else notificationRepository.notifications.map { it.filter { it.packageName == searchable.`package` && !it.isGroupSummary } }
|
||||
if (searchable !is Application) emptyFlow()
|
||||
else notificationRepository.notifications.map { it.filter { it.packageName == searchable.componentName.packageName && !it.isGroupSummary } }
|
||||
}
|
||||
|
||||
val shortcuts = searchable.map {
|
||||
if (it !is LauncherApp) emptyList()
|
||||
else appShortcutRepository.getShortcutsForActivity(it.launcherActivityInfo, 5)
|
||||
if (it !is Application) emptyList()
|
||||
else appShortcutRepository
|
||||
.findMany(
|
||||
componentName = it.componentName,
|
||||
user = it.user,
|
||||
manifest = true,
|
||||
dynamic = true,
|
||||
).first()
|
||||
}
|
||||
|
||||
open fun launch(context: Context, bounds: Rect? = null): Boolean {
|
||||
@ -120,7 +122,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
||||
if (searchable.launch(context, bundle)) {
|
||||
favoritesService.reportLaunch(searchable)
|
||||
return true
|
||||
} else if (searchable is LauncherApp || searchable is AppShortcut) {
|
||||
} else if (searchable is Application || searchable is AppShortcut) {
|
||||
favoritesService.reset(searchable)
|
||||
}
|
||||
return false
|
||||
@ -130,9 +132,8 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
||||
notificationRepository.cancelNotification(notification)
|
||||
}
|
||||
|
||||
fun getShortcutIcon(context: Context, shortcut: ShortcutInfo): Drawable? {
|
||||
val launcherApps = context.getSystemService<LauncherApps>() ?: return null
|
||||
return launcherApps.getShortcutIconDrawable(shortcut, 0)
|
||||
fun getShortcutIcon(context: Context, shortcut: AppShortcut, size: Int): Flow<LauncherIcon?> {
|
||||
return iconService.getIcon(shortcut, size)
|
||||
}
|
||||
|
||||
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
|
||||
@ -151,10 +152,18 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
|
||||
shortcut.launch(context, null)
|
||||
}
|
||||
|
||||
fun delete() {
|
||||
fun delete(context: Context) {
|
||||
val searchable = searchable.value ?: return
|
||||
if (searchable is File) fileRepository.deleteFile(searchable)
|
||||
if (searchable is LauncherShortcut) appShortcutRepository.removePinnedShortcut(searchable)
|
||||
if (searchable is File) {
|
||||
viewModelScope.launch {
|
||||
searchable.delete(context.applicationContext)
|
||||
}
|
||||
}
|
||||
if (searchable is AppShortcut) {
|
||||
viewModelScope.launch {
|
||||
searchable.delete(context.applicationContext)
|
||||
}
|
||||
}
|
||||
favoritesService.reset(searchable)
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.common.grid
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
@ -42,18 +40,17 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.data.Website
|
||||
import de.mm20.launcher2.search.data.Wikipedia
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.Article
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.Website
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.component.LocalIconShape
|
||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||
@ -66,13 +63,12 @@ import de.mm20.launcher2.ui.launcher.search.files.FileItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.search.listItemViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.shortcut.ShortcutItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.search.wikipedia.ArticleItemGridPopup
|
||||
import de.mm20.launcher2.ui.launcher.transitions.EnterHomeTransitionParams
|
||||
import de.mm20.launcher2.ui.launcher.transitions.HandleEnterHomeTransition
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.ui.locals.LocalWindowSize
|
||||
import de.mm20.launcher2.ui.overlays.Overlay
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
|
||||
@ -121,9 +117,9 @@ fun GridItem(
|
||||
|
||||
val windowSize = LocalWindowSize.current
|
||||
|
||||
if (item is LauncherApp) {
|
||||
if (item is Application) {
|
||||
HandleEnterHomeTransition {
|
||||
val cn = ComponentName(item.`package`, item.activity)
|
||||
val cn = item.componentName
|
||||
if (
|
||||
it.componentName == cn &&
|
||||
bounds.right > 0f && bounds.left < windowSize.width &&
|
||||
@ -240,7 +236,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
|
||||
)
|
||||
) {
|
||||
when (searchable) {
|
||||
is LauncherApp -> {
|
||||
is Application -> {
|
||||
AppItemGridPopup(
|
||||
app = searchable,
|
||||
show = show,
|
||||
@ -264,9 +260,9 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
|
||||
)
|
||||
}
|
||||
|
||||
is Wikipedia -> {
|
||||
WikipediaItemGridPopup(
|
||||
wikipedia = searchable,
|
||||
is Article -> {
|
||||
ArticleItemGridPopup(
|
||||
article = searchable,
|
||||
show = show,
|
||||
animationProgress = animationProgress.value,
|
||||
origin = origin,
|
||||
|
||||
@ -9,9 +9,11 @@ import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.*
|
||||
import de.mm20.launcher2.ui.component.InnerCard
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem
|
||||
|
||||
@ -17,16 +17,20 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.Message
|
||||
import androidx.compose.material.icons.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.rounded.Call
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.Email
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.MoreHoriz
|
||||
import androidx.compose.material.icons.rounded.OpenInNew
|
||||
import androidx.compose.material.icons.rounded.Place
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.StarOutline
|
||||
import androidx.compose.material.icons.rounded.Visibility
|
||||
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||
import androidx.compose.material.icons.rounded.Whatsapp
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
@ -36,6 +40,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
@ -49,7 +54,8 @@ import androidx.compose.ui.unit.roundToIntRect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.ContactInfoType
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||
import de.mm20.launcher2.ui.component.Chip
|
||||
@ -144,164 +150,38 @@ fun ContactItem(
|
||||
}
|
||||
|
||||
AnimatedVisibility(showDetails) {
|
||||
val groups = remember {
|
||||
contact.contactInfos.groupBy { it.type }
|
||||
}
|
||||
Column {
|
||||
|
||||
if (contact.phones.isNotEmpty()) {
|
||||
for ((type, items) in groups) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.Call, contentDescription = null)
|
||||
Icon(when(type) {
|
||||
ContactInfoType.Phone -> Icons.Rounded.Call
|
||||
ContactInfoType.Message -> Icons.AutoMirrored.Rounded.Message
|
||||
ContactInfoType.Email -> Icons.Rounded.Email
|
||||
ContactInfoType.Postal -> Icons.Rounded.Home
|
||||
ContactInfoType.Telegram -> Icons.Rounded.Telegram
|
||||
ContactInfoType.Whatsapp -> Icons.Rounded.Whatsapp
|
||||
ContactInfoType.Signal -> Icons.Rounded.Signal
|
||||
ContactInfoType.Other -> Icons.Rounded.MoreHoriz
|
||||
}, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.phones.toList()) {
|
||||
items(items.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setData(Uri.parse(it.data))
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contact.emails.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.Email, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.emails.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setData(Uri.parse(it.data))
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contact.signal.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.Signal, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.signal.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setData(Uri.parse(it.data))
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contact.telegram.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.Telegram, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.telegram.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setData(Uri.parse(it.data))
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contact.whatsapp.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.WhatsApp, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.whatsapp.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setData(Uri.parse(it.data))
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contact.postals.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.Place, contentDescription = null)
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(contact.postals.toList()) {
|
||||
Chip(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
text = it.label,
|
||||
onClick = {
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setData(Uri.parse(it.data))
|
||||
)
|
||||
context.tryStartActivity(it.intent)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.roundToIntRect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
@ -236,7 +236,7 @@ fun FileItem(
|
||||
onDismissRequest = { showConfirmDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.delete()
|
||||
viewModel.delete(context)
|
||||
showConfirmDialog = false
|
||||
}) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
|
||||
@ -53,10 +53,7 @@ import androidx.compose.ui.unit.roundToIntRect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||
import de.mm20.launcher2.search.data.LegacyShortcut
|
||||
import de.mm20.launcher2.search.data.UnavailableShortcut
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.animation.animateTextStyleAsState
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
@ -101,7 +98,7 @@ fun AppShortcutItem(
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
AnimatedVisibility(showDetails && shortcut is UnavailableShortcut) {
|
||||
AnimatedVisibility(showDetails && shortcut.isUnavailable) {
|
||||
MissingPermissionBanner(
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
|
||||
text = stringResource(R.string.shortcut_unavailable_description, stringResource(R.string.app_name)),
|
||||
@ -213,7 +210,7 @@ fun AppShortcutItem(
|
||||
action = { sheetManager.showCustomizeSearchableModal(shortcut) }
|
||||
))
|
||||
|
||||
if (shortcut is LauncherShortcut && shortcut.launcherShortcut.isPinned) {
|
||||
if (shortcut.canDelete) {
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_delete),
|
||||
icon = Icons.Rounded.Delete,
|
||||
@ -276,7 +273,7 @@ fun AppShortcutItem(
|
||||
text = { Text(stringResource(R.string.alert_delete_shortcut, shortcut.label)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.delete()
|
||||
viewModel.delete(context)
|
||||
requestDelete = false
|
||||
}) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
@ -330,9 +327,3 @@ fun ShortcutItemGridPopup(
|
||||
}
|
||||
}
|
||||
|
||||
val AppShortcut.packageName: String?
|
||||
get() = when (this) {
|
||||
is LegacyShortcut -> intent.`package`
|
||||
is LauncherShortcut -> launcherShortcut.`package`
|
||||
else -> null
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.roundToIntRect
|
||||
import coil.compose.AsyncImage
|
||||
import de.mm20.launcher2.search.data.Website
|
||||
import de.mm20.launcher2.search.Website
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
import de.mm20.launcher2.ui.component.Toolbar
|
||||
@ -64,13 +64,13 @@ fun WebsiteItem(
|
||||
viewModel.launch(context)
|
||||
}
|
||||
) {
|
||||
if (website.image.isNotBlank()) {
|
||||
if (website.imageUrl != null) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16f / 9f)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||
model = website.image,
|
||||
model = website.imageUrl,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null
|
||||
)
|
||||
@ -93,7 +93,7 @@ fun WebsiteItem(
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
text = website.description,
|
||||
text = website.description ?: "",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.roundToIntRect
|
||||
import coil.compose.AsyncImage
|
||||
import de.mm20.launcher2.search.data.Wikipedia
|
||||
import de.mm20.launcher2.search.Article
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
import de.mm20.launcher2.ui.component.Toolbar
|
||||
@ -46,18 +46,18 @@ import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.ui.utils.htmlToAnnotatedString
|
||||
|
||||
@Composable
|
||||
fun WikipediaItem(
|
||||
fun ArticleItem(
|
||||
modifier: Modifier = Modifier,
|
||||
wikipedia: Wikipedia,
|
||||
article: Article,
|
||||
onBack: (() -> Unit)? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${wikipedia.key}")
|
||||
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${article.key}")
|
||||
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
|
||||
|
||||
LaunchedEffect(wikipedia, iconSize) {
|
||||
viewModel.init(wikipedia, iconSize.toInt())
|
||||
LaunchedEffect(article, iconSize) {
|
||||
viewModel.init(article, iconSize.toInt())
|
||||
}
|
||||
|
||||
Column(
|
||||
@ -65,13 +65,13 @@ fun WikipediaItem(
|
||||
viewModel.launch(context)
|
||||
}
|
||||
) {
|
||||
if (!wikipedia.image.isNullOrEmpty()) {
|
||||
if (!article.imageUrl.isNullOrEmpty()) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16f / 9f)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||
model = wikipedia.image,
|
||||
model = article.imageUrl,
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null
|
||||
)
|
||||
@ -80,7 +80,7 @@ fun WikipediaItem(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = wikipedia.label,
|
||||
text = article.label,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
val tags by viewModel.tags.collectAsState(emptyList())
|
||||
@ -100,7 +100,7 @@ fun WikipediaItem(
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
text = htmlToAnnotatedString(wikipedia.text),
|
||||
text = htmlToAnnotatedString(article.text),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
@ -134,7 +134,7 @@ fun WikipediaItem(
|
||||
label = stringResource(R.string.menu_share),
|
||||
icon = Icons.Rounded.Share,
|
||||
action = {
|
||||
wikipedia.share(context)
|
||||
article.share(context)
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -143,7 +143,7 @@ fun WikipediaItem(
|
||||
toolbarActions.add(DefaultToolbarAction(
|
||||
label = stringResource(R.string.menu_customize),
|
||||
icon = Icons.Rounded.Edit,
|
||||
action = { sheetManager.showCustomizeSearchableModal(wikipedia) }
|
||||
action = { sheetManager.showCustomizeSearchableModal(article) }
|
||||
))
|
||||
|
||||
Toolbar(
|
||||
@ -160,8 +160,8 @@ fun WikipediaItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WikipediaItemGridPopup(
|
||||
wikipedia: Wikipedia,
|
||||
fun ArticleItemGridPopup(
|
||||
article: Article,
|
||||
show: MutableTransitionState<Boolean>,
|
||||
animationProgress: Float,
|
||||
origin: Rect,
|
||||
@ -178,10 +178,10 @@ fun WikipediaItemGridPopup(
|
||||
shrinkTowards = Alignment.Center,
|
||||
) { origin.roundToIntRect().size },
|
||||
) {
|
||||
WikipediaItem(
|
||||
ArticleItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
wikipedia = wikipedia,
|
||||
article = article,
|
||||
onBack = onDismiss
|
||||
)
|
||||
}
|
||||
@ -74,7 +74,7 @@ import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.data.UserCalendar
|
||||
import de.mm20.launcher2.calendar.UserCalendar
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||
import de.mm20.launcher2.ui.component.LargeMessage
|
||||
|
||||
@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.launcher.sheets
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@ -64,6 +65,7 @@ import androidx.compose.ui.graphics.drawOutline
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@ -73,6 +75,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.toOffset
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
@ -647,7 +650,6 @@ fun ShortcutPicker(viewModel: EditFavoritesSheetVM, paddingValues: PaddingValues
|
||||
|
||||
}
|
||||
|
||||
val iconSize = 48.dp.toPixels().roundToInt()
|
||||
val activity = LocalLifecycleOwner.current as AppCompatActivity
|
||||
LazyColumn(
|
||||
contentPadding = paddingValues
|
||||
@ -664,31 +666,28 @@ fun ShortcutPicker(viewModel: EditFavoritesSheetVM, paddingValues: PaddingValues
|
||||
}
|
||||
}
|
||||
items(shortcutActivities) {
|
||||
val icon by remember(it.key) { viewModel.getIcon(it, iconSize) }.collectAsState(null)
|
||||
val badge by remember(it.key) { viewModel.getBadge(it) }.collectAsState(null)
|
||||
val icon by remember(it) { it.getIcon(context) }.collectAsState(null)
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
onClick = {
|
||||
val launcherApps =
|
||||
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
val sender =
|
||||
launcherApps.getShortcutConfigActivityIntent(it.launcherActivityInfo)
|
||||
?: return@OutlinedCard
|
||||
activityLauncher.launch(IntentSenderRequest.Builder(sender).build(), null)
|
||||
val intent = it.getIntent(context) ?: return@OutlinedCard run {
|
||||
Log.e("MM20", "Couldn't get intent for shortcut")
|
||||
}
|
||||
activityLauncher.launch(IntentSenderRequest.Builder(intent).build(), null)
|
||||
}) {
|
||||
Row(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ShapedLauncherIcon(
|
||||
size = 48.dp,
|
||||
icon = { icon },
|
||||
badge = { badge },
|
||||
AsyncImage(
|
||||
model = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Text(
|
||||
text = it.labelOverride ?: it.label,
|
||||
text = it.label,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
|
||||
@ -19,7 +19,7 @@ import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.appshortcuts.AppShortcut
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import de.mm20.launcher2.search.data.Tag
|
||||
import de.mm20.launcher2.services.favorites.FavoritesService
|
||||
@ -210,7 +210,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
|
||||
fun createShortcut(context: Context, data: Intent?) {
|
||||
data ?: return cancelPickShortcut()
|
||||
|
||||
val shortcut = AppShortcut.fromPinRequestIntent(context, data)
|
||||
val shortcut = AppShortcut(context, data)
|
||||
|
||||
if (shortcut == null) {
|
||||
cancelPickShortcut()
|
||||
|
||||
@ -9,11 +9,11 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.calendar.CalendarRepository
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.services.favorites.FavoritesService
|
||||
import de.mm20.launcher2.widgets.CalendarWidget
|
||||
import de.mm20.launcher2.widgets.CalendarWidgetConfig
|
||||
@ -34,14 +34,14 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val calendarRepository: CalendarRepository by inject()
|
||||
private val favoritesService: FavoritesService by inject()
|
||||
private val searchableRepository: SearchableRepository by inject()
|
||||
private val searchableRepository: SavableSearchableRepository by inject()
|
||||
|
||||
private val widgetConfig = MutableStateFlow(CalendarWidgetConfig())
|
||||
|
||||
val calendarEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
||||
val pinnedCalendarEvents =
|
||||
favoritesService.getFavorites(
|
||||
includeTypes = listOf(CalendarEvent.Domain),
|
||||
includeTypes = listOf("calendar"),
|
||||
automaticallySorted = true,
|
||||
manuallySorted = true,
|
||||
).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
@ -164,12 +164,14 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
||||
suspend fun onActive() {
|
||||
selectDate(LocalDate.now())
|
||||
widgetConfig.collectLatest { config ->
|
||||
calendarRepository.getUpcomingEvents(
|
||||
calendarRepository.findMany(
|
||||
from = System.currentTimeMillis(),
|
||||
to = System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000L,
|
||||
excludeAllDayEvents = !config.allDayEvents,
|
||||
excludeCalendars = config.excludedCalendarIds,
|
||||
).collectLatest { events ->
|
||||
searchableRepository.getKeys(
|
||||
includeTypes = listOf(CalendarEvent.Domain),
|
||||
includeTypes = listOf("calendar"),
|
||||
hidden = true,
|
||||
limit = 9999,
|
||||
).collectLatest { hidden ->
|
||||
|
||||
@ -11,7 +11,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
|
||||
import de.mm20.launcher2.services.favorites.FavoritesService
|
||||
|
||||
@ -3,13 +3,13 @@ package de.mm20.launcher2.ui.settings.debug
|
||||
import androidx.lifecycle.ViewModel
|
||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
||||
import de.mm20.launcher2.icons.IconService
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class DebugSettingsScreenVM: ViewModel(), KoinComponent {
|
||||
|
||||
private val searchableRepository: SearchableRepository by inject()
|
||||
private val searchableRepository: SavableSearchableRepository by inject()
|
||||
private val customAttributesRepository: CustomAttributesRepository by inject()
|
||||
private val iconService: IconService by inject()
|
||||
suspend fun cleanUpDatabase(): Int {
|
||||
|
||||
@ -3,7 +3,7 @@ package de.mm20.launcher2.ui.settings.gestures
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||
import de.mm20.launcher2.icons.IconService
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
@ -24,7 +24,7 @@ import org.koin.core.component.inject
|
||||
class GestureSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
private val searchableRepository: SearchableRepository by inject()
|
||||
private val searchableRepository: SavableSearchableRepository by inject()
|
||||
private val iconService: IconService by inject()
|
||||
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Accessibility)
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
package de.mm20.launcher2.ui.settings.hiddenitems
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.applications.AppRepository
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||
import de.mm20.launcher2.icons.IconService
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.Application
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@ -30,16 +27,16 @@ import org.koin.core.component.inject
|
||||
|
||||
class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
private val appRepository: AppRepository by inject()
|
||||
private val searchableRepository: SearchableRepository by inject()
|
||||
private val searchableRepository: SavableSearchableRepository by inject()
|
||||
private val iconService: IconService by inject()
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
val allApps = appRepository.getAllInstalledApps().map {
|
||||
val allApps = appRepository.findMany().map {
|
||||
withContext(Dispatchers.Default) { it.sorted() }
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
val hiddenItems: StateFlow<List<SavableSearchable>> = flow {
|
||||
val hidden =
|
||||
searchableRepository.get(hidden = true).first().filter { it !is LauncherApp }.sorted()
|
||||
searchableRepository.get(hidden = true).first().filter { it !is Application }.sorted()
|
||||
emit(hidden)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
@ -67,15 +64,8 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
searchable.launch(context, bundle)
|
||||
}
|
||||
|
||||
fun openAppInfo(context: Context, app: LauncherApp) {
|
||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||
|
||||
launcherApps.startAppDetailsActivity(
|
||||
ComponentName(app.`package`, app.activity),
|
||||
app.getUser(),
|
||||
null,
|
||||
null
|
||||
)
|
||||
fun openAppInfo(context: Context, app: Application) {
|
||||
app.openAppDetails(context)
|
||||
}
|
||||
|
||||
val hiddenItemsButton = dataStore.data.map { it.searchBar.hiddenItemsButton }
|
||||
|
||||
@ -12,7 +12,7 @@ import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.services.favorites.FavoritesService
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@ -224,7 +224,7 @@ class IconsSettingsScreenVM(
|
||||
fun getPreviewIcons(size: Int): Flow<List<LauncherIcon?>> {
|
||||
return columnCount.flatMapLatest { cols ->
|
||||
favoritesService.getFavorites(
|
||||
includeTypes = listOf(LauncherApp.Domain),
|
||||
includeTypes = listOf("app"),
|
||||
limit = cols,
|
||||
manuallySorted = true,
|
||||
automaticallySorted = true,
|
||||
|
||||
@ -12,6 +12,7 @@ import de.mm20.launcher2.music.MusicService
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.AppProfile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@ -49,8 +50,8 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
loading.value = true
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val musicApps = musicService.getInstalledPlayerPackages()
|
||||
val allApps = appRepository.getAllInstalledApps().first().filter { it.isMainProfile }
|
||||
.distinctBy { it.`package` }
|
||||
val allApps = appRepository.findMany().first().filter { it.profile == AppProfile.Personal }
|
||||
.distinctBy { it.componentName.packageName }
|
||||
val settings = dataStore.data.map { it.musicWidget }.first()
|
||||
val allowList = settings.allowListList
|
||||
val denyList = settings.denyListList
|
||||
@ -58,10 +59,10 @@ class MediaIntegrationSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
appList.value = allApps.map {
|
||||
AppListItem(
|
||||
label = it.label,
|
||||
packageName = it.`package`,
|
||||
isMusicApp = musicApps.contains(it.`package`),
|
||||
isChecked = allowList.contains(it.`package`) || (!denyList.contains(it.`package`) && musicApps.contains(
|
||||
it.`package`
|
||||
packageName = it.componentName.packageName,
|
||||
isMusicApp = musicApps.contains(it.componentName.packageName),
|
||||
isChecked = allowList.contains(it.componentName.packageName) || (!denyList.contains(it.componentName.packageName) && musicApps.contains(
|
||||
it.componentName.packageName
|
||||
)),
|
||||
icon = iconService.getIcon(it, (32 * density).roundToInt())
|
||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(10000))
|
||||
|
||||
@ -55,7 +55,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
allTags = tagService.getAllTags().first().toSet()
|
||||
val items = if (tag != null) tagService.getTaggedItems(tag).first() else emptyList()
|
||||
val apps = appRepository.getAllInstalledApps().first().sorted()
|
||||
val apps = appRepository.findMany().first().sorted()
|
||||
taggedItems = items
|
||||
taggableApps = apps.map { app -> TaggableItem(app, items.any { app.key == it.key }) }
|
||||
taggableOther = items.mapNotNull { item ->
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.mm20.launcher2.appshortcuts.R
|
||||
import de.mm20.launcher2.base.R
|
||||
import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TintedIconLayer
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
|
||||
interface AppShortcut: SavableSearchable {
|
||||
interface AppShortcut : SavableSearchable {
|
||||
|
||||
val appName: String?
|
||||
val componentName: ComponentName?
|
||||
val packageName: String?
|
||||
|
||||
override val preferDetailsOverLaunch: Boolean
|
||||
get() = false
|
||||
@ -27,10 +28,13 @@ interface AppShortcut: SavableSearchable {
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromPinRequestIntent(context: Context, data: Intent): AppShortcut? {
|
||||
return LauncherShortcut.fromPinRequestIntent(context, data)
|
||||
?: LegacyShortcut.fromPinRequestIntent(context, data)
|
||||
}
|
||||
}
|
||||
val canDelete: Boolean
|
||||
get() = false
|
||||
|
||||
suspend fun delete(context: Context) {}
|
||||
|
||||
val isUnavailable: Boolean
|
||||
get() = false
|
||||
|
||||
val profile: AppProfile
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
17
core/base/src/main/java/de/mm20/launcher2/search/Article.kt
Normal file
17
core/base/src/main/java/de/mm20/launcher2/search/Article.kt
Normal 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
|
||||
}
|
||||
@ -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) {}
|
||||
}
|
||||
35
core/base/src/main/java/de/mm20/launcher2/search/Contact.kt
Normal file
35
core/base/src/main/java/de/mm20/launcher2/search/Contact.kt
Normal 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
|
||||
}
|
||||
@ -1,13 +1,12 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.base.R
|
||||
import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TintedIconLayer
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
interface File : SavableSearchable {
|
||||
val path: String
|
||||
@ -21,7 +20,7 @@ interface File : SavableSearchable {
|
||||
override val preferDetailsOverLaunch: Boolean
|
||||
get() = false
|
||||
|
||||
open val providerIconRes: Int?
|
||||
val providerIconRes: Int?
|
||||
get() = null
|
||||
|
||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||
@ -8,8 +8,6 @@ import de.mm20.launcher2.ktx.romanize
|
||||
import java.text.Collator
|
||||
|
||||
interface SavableSearchable : Searchable, Comparable<SavableSearchable> {
|
||||
|
||||
val domain: String
|
||||
val key: String
|
||||
|
||||
val label: String
|
||||
@ -41,4 +39,11 @@ interface SavableSearchable : Searchable, Comparable<SavableSearchable> {
|
||||
.compare(label1.romanize(), label2.romanize())
|
||||
}
|
||||
|
||||
val domain: String
|
||||
fun getSerializer(): SearchableSerializer
|
||||
|
||||
interface Companion {
|
||||
val Domain: String
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
interface SearchableDeserializer {
|
||||
fun deserialize(serialized: String): SavableSearchable?
|
||||
suspend fun deserialize(serialized: String): SavableSearchable?
|
||||
}
|
||||
|
||||
class NullDeserializer: SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable? {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@ -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>>
|
||||
}
|
||||
18
core/base/src/main/java/de/mm20/launcher2/search/Website.kt
Normal file
18
core/base/src/main/java/de/mm20/launcher2/search/Website.kt
Normal 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) {}
|
||||
}
|
||||
@ -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("")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,8 +6,8 @@ import android.content.Intent
|
||||
import android.content.pm.LauncherApps
|
||||
import android.os.Process
|
||||
import androidx.core.content.getSystemService
|
||||
import de.mm20.launcher2.ktx.getSerialNumber
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -16,51 +16,18 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* A fake implementation of [AppRepository] to simulate many installed apps.
|
||||
* App repository that returns a fixed number of fake apps to simulate a large number of apps.
|
||||
*/
|
||||
class FakeAppRepository(private val context: Context, private val fakePackages: Int) : AppRepository {
|
||||
class FakeAppRepository(private val context: Context, private val fakePackages: Int) : SearchableRepository<Application> {
|
||||
|
||||
|
||||
private val fakeApp: LauncherApp
|
||||
|
||||
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>> {
|
||||
override fun search(query: String): Flow<ImmutableList<Application>> {
|
||||
return if (query.isEmpty()) {
|
||||
getAllInstalledApps().map { it.toImmutableList() }
|
||||
buildList {
|
||||
repeat(fakePackages) {
|
||||
add(FakeApp())
|
||||
}
|
||||
}.toImmutableList().let { flowOf(it) }
|
||||
} else {
|
||||
flowOf(persistentListOf())
|
||||
}
|
||||
|
||||
@ -10,28 +10,34 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Process
|
||||
import android.os.UserHandle
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.ktx.normalize
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.text.similarity.FuzzyScore
|
||||
import java.util.Locale
|
||||
|
||||
interface AppRepository {
|
||||
fun getAllInstalledApps(): Flow<List<LauncherApp>>
|
||||
fun getSuspendedPackages(): Flow<List<String>>
|
||||
fun search(query: String): Flow<ImmutableList<LauncherApp>>
|
||||
interface AppRepository : SearchableRepository<Application> {
|
||||
override fun search(query: String): Flow<ImmutableList<Application>>
|
||||
|
||||
fun findMany(): Flow<ImmutableList<Application>>
|
||||
}
|
||||
|
||||
internal class AppRepositoryImpl(
|
||||
private val context: Context,
|
||||
) : AppRepository {
|
||||
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
||||
|
||||
private val launcherApps =
|
||||
context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
@ -40,9 +46,10 @@ internal class AppRepositoryImpl(
|
||||
private val suspendedPackages = MutableStateFlow<List<String>>(emptyList())
|
||||
|
||||
|
||||
private val profiles: List<UserHandle> =
|
||||
launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
|
||||
private val profiles: List<UserHandle>
|
||||
get() = launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle())
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
init {
|
||||
launcherApps.registerCallback(object : LauncherApps.Callback() {
|
||||
@ -51,15 +58,23 @@ internal class AppRepositoryImpl(
|
||||
user: UserHandle,
|
||||
replacing: Boolean
|
||||
) {
|
||||
installedApps.value =
|
||||
installedApps.value.filter { !packageNames.contains(it.`package`) }
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
installedApps.value =
|
||||
installedApps.value.filter { !packageNames.contains(it.componentName.packageName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPackageChanged(packageName: String, user: UserHandle) {
|
||||
val apps = installedApps.value.toMutableList()
|
||||
apps.removeAll { packageName == it.`package` }
|
||||
apps.addAll(getApplications(packageName))
|
||||
installedApps.value = apps
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
val apps = installedApps.value.toMutableList()
|
||||
apps.removeAll { packageName == it.componentName.packageName }
|
||||
apps.addAll(getApplications(packageName))
|
||||
installedApps.value = apps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPackagesAvailable(
|
||||
@ -67,23 +82,35 @@ internal class AppRepositoryImpl(
|
||||
user: UserHandle,
|
||||
replacing: Boolean
|
||||
) {
|
||||
val apps = installedApps.value.toMutableList()
|
||||
for (packageName in packageNames) {
|
||||
apps.addAll(getApplications(packageName))
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
val apps = installedApps.value.toMutableList()
|
||||
for (packageName in packageNames) {
|
||||
apps.addAll(getApplications(packageName))
|
||||
}
|
||||
installedApps.value = apps
|
||||
}
|
||||
}
|
||||
installedApps.value = apps
|
||||
}
|
||||
|
||||
override fun onPackageAdded(packageName: String, user: UserHandle) {
|
||||
Log.d("MM20", "App installed: $packageName")
|
||||
val apps = installedApps.value.toMutableList()
|
||||
apps.addAll(getApplications(packageName))
|
||||
installedApps.value = apps
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
val apps = installedApps.value.toMutableList()
|
||||
apps.addAll(getApplications(packageName))
|
||||
installedApps.value = apps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPackageRemoved(packageName: String, user: UserHandle) {
|
||||
installedApps.value =
|
||||
installedApps.value.filter { packageName != (it.`package`) || it.getUser() != user }
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
installedApps.value =
|
||||
installedApps.value.filter { packageName != (it.componentName.packageName) || it.user != user }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShortcutsChanged(
|
||||
@ -91,40 +118,55 @@ internal class AppRepositoryImpl(
|
||||
shortcuts: MutableList<ShortcutInfo>,
|
||||
user: UserHandle
|
||||
) {
|
||||
super.onShortcutsChanged(packageName, shortcuts, user)
|
||||
onPackageChanged(packageName, user)
|
||||
}
|
||||
|
||||
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
|
||||
super.onPackagesSuspended(packageNames, user)
|
||||
packageNames ?: return
|
||||
suspendedPackages.value = suspendedPackages.value + packageNames
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
installedApps.value = installedApps.value.map {
|
||||
if (packageNames.contains(it.componentName.packageName)) {
|
||||
it.copy(isSuspended = true)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPackagesUnsuspended(
|
||||
packageNames: Array<out String>?,
|
||||
user: UserHandle?
|
||||
) {
|
||||
super.onPackagesUnsuspended(packageNames, user)
|
||||
packageNames ?: return
|
||||
suspendedPackages.value =
|
||||
suspendedPackages.value.filter { packageNames.contains(it) }
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
installedApps.value = installedApps.value.map {
|
||||
if (packageNames.contains(it.componentName.packageName)) {
|
||||
it.copy(isSuspended = false)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, Handler(Looper.getMainLooper()))
|
||||
val apps = profiles.map { p ->
|
||||
try {
|
||||
launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) }
|
||||
} catch (e: SecurityException) {
|
||||
emptyList()
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
val apps = profiles.map { p ->
|
||||
try {
|
||||
launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) }
|
||||
} catch (e: SecurityException) {
|
||||
emptyList()
|
||||
}
|
||||
}.flatten()
|
||||
installedApps.value = apps
|
||||
}
|
||||
}.flatten()
|
||||
installedApps.value = apps
|
||||
}
|
||||
|
||||
|
||||
override fun getSuspendedPackages(): Flow<List<String>> {
|
||||
return suspendedPackages
|
||||
}
|
||||
}
|
||||
|
||||
private fun getApplications(packageName: String): List<LauncherApp> {
|
||||
@ -151,6 +193,10 @@ internal class AppRepositoryImpl(
|
||||
return LauncherApp(context, launcherActivityInfo)
|
||||
}
|
||||
|
||||
override fun findMany(): Flow<ImmutableList<Application>> {
|
||||
return installedApps.map { it.toImmutableList() }
|
||||
}
|
||||
|
||||
override fun search(query: String): Flow<ImmutableList<LauncherApp>> {
|
||||
return installedApps.map { apps ->
|
||||
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 {
|
||||
val normalizedLabel = label.normalize()
|
||||
val normalizedQuery = query.normalize()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.applications
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
@ -15,8 +15,8 @@ class LauncherAppSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as LauncherApp
|
||||
val json = JSONObject()
|
||||
json.put("package", searchable.`package`)
|
||||
json.put("activity", searchable.activity)
|
||||
json.put("package", searchable.componentName.packageName)
|
||||
json.put("activity", searchable.componentName.className)
|
||||
json.put("user", searchable.userSerialNumber)
|
||||
return json.toString()
|
||||
}
|
||||
@ -26,7 +26,7 @@ class LauncherAppSerializer : SearchableSerializer {
|
||||
}
|
||||
|
||||
class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable? {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
val json = JSONObject(serialized)
|
||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||
val userManager = context.getSystemService<UserManager>()!!
|
||||
@ -1,9 +1,10 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.applications
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.LauncherActivityInfo
|
||||
import android.content.pm.LauncherApps
|
||||
@ -13,43 +14,56 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.os.UserHandle
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.getSystemService
|
||||
import de.mm20.launcher2.applications.R
|
||||
import de.mm20.launcher2.compat.PackageManagerCompat
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.icons.StaticIconLayer
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TintedIconLayer
|
||||
import de.mm20.launcher2.icons.TransparentLayer
|
||||
import de.mm20.launcher2.ktx.getSerialNumber
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.AppProfile
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.StoreLink
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class LauncherApp(
|
||||
val launcherActivityInfo: LauncherActivityInfo,
|
||||
override val label: String,
|
||||
val `package`: String,
|
||||
val activity: String,
|
||||
val flags: Int,
|
||||
val version: String?,
|
||||
internal data class LauncherApp(
|
||||
private val launcherActivityInfo: LauncherActivityInfo,
|
||||
override val versionName: String?,
|
||||
override val isSuspended: Boolean = false,
|
||||
internal val userSerialNumber: Long,
|
||||
override val labelOverride: String? = null,
|
||||
) : SavableSearchable {
|
||||
) : Application {
|
||||
|
||||
constructor(context: Context, launcherActivityInfo: LauncherActivityInfo): this(
|
||||
override val componentName: ComponentName
|
||||
get() = launcherActivityInfo.componentName
|
||||
|
||||
override val label: String = launcherActivityInfo.label.toString()
|
||||
|
||||
|
||||
constructor(context: Context, launcherActivityInfo: LauncherActivityInfo) : this(
|
||||
launcherActivityInfo,
|
||||
label = launcherActivityInfo.label.toString(),
|
||||
`package` = launcherActivityInfo.applicationInfo.packageName,
|
||||
activity = launcherActivityInfo.name,
|
||||
flags = launcherActivityInfo.applicationInfo.flags,
|
||||
version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
|
||||
versionName = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
|
||||
userSerialNumber = launcherActivityInfo.user.getSerialNumber(context)
|
||||
)
|
||||
|
||||
val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
|
||||
override val user: UserHandle
|
||||
get() = launcherActivityInfo.user
|
||||
|
||||
val canUninstall: Boolean
|
||||
get() = flags and ApplicationInfo.FLAG_SYSTEM == 0 && isMainProfile
|
||||
private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
|
||||
|
||||
override val profile: AppProfile
|
||||
get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work
|
||||
|
||||
override val isSystemApp: Boolean = launcherActivityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0
|
||||
|
||||
override val canUninstall: Boolean
|
||||
get() = !isSystemApp && isMainProfile
|
||||
|
||||
override val domain: String = Domain
|
||||
override val preferDetailsOverLaunch: Boolean = false
|
||||
@ -59,22 +73,10 @@ data class LauncherApp(
|
||||
}
|
||||
|
||||
override val key: String
|
||||
get() = if (isMainProfile) "${domain}://$`package`:$activity" else "${domain}://$`package`:$activity:${userSerialNumber}"
|
||||
// For backwards compatibility, user serial number is not included in main profile
|
||||
get() = if (isMainProfile) "${domain}://${componentName.packageName}:${componentName.packageName}"
|
||||
else "${domain}://${componentName.packageName}:${componentName.className}:${userSerialNumber}"
|
||||
|
||||
fun getUser(): UserHandle? {
|
||||
return launcherActivityInfo.user
|
||||
}
|
||||
|
||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TintedIconLayer(
|
||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
|
||||
scale = 0.65f,
|
||||
color = 0xff3dda84.toInt(),
|
||||
),
|
||||
backgroundLayer = ColorLayer(0xff3dda84.toInt())
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun loadIcon(
|
||||
context: Context,
|
||||
@ -132,7 +134,7 @@ data class LauncherApp(
|
||||
}
|
||||
try {
|
||||
launcherApps.startMainActivity(
|
||||
ComponentName(`package`, activity),
|
||||
componentName,
|
||||
launcherActivityInfo.user,
|
||||
null,
|
||||
options
|
||||
@ -145,11 +147,15 @@ data class LauncherApp(
|
||||
return true
|
||||
}
|
||||
|
||||
fun getStoreDetails(context: Context): StoreLink? {
|
||||
override fun getStoreDetails(context: Context): StoreLink? {
|
||||
val pm = context.packageManager
|
||||
return try {
|
||||
val installSourceInfo = PackageManagerCompat.getInstallSource(pm, `package`)
|
||||
getStoreLinkForInstaller(installSourceInfo.initiatingPackageName, `package`)
|
||||
val installSourceInfo =
|
||||
PackageManagerCompat.getInstallSource(pm, componentName.packageName)
|
||||
getStoreLinkForInstaller(
|
||||
installSourceInfo.initiatingPackageName,
|
||||
componentName.packageName
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
@ -157,37 +163,33 @@ data class LauncherApp(
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstall(context: Context) {
|
||||
override fun uninstall(context: Context) {
|
||||
val intent = Intent(Intent.ACTION_DELETE)
|
||||
intent.data = Uri.parse("package:$`package`")
|
||||
intent.data = Uri.parse("package:${componentName.packageName}")
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun openAppInfo(context: Context) {
|
||||
override fun openAppDetails(context: Context) {
|
||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||
|
||||
launcherApps.startAppDetailsActivity(
|
||||
ComponentName(`package`, activity),
|
||||
getUser(),
|
||||
componentName,
|
||||
user,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun shareApkFile(context: Context) {
|
||||
override val canShareApk: Boolean = true
|
||||
override suspend fun shareApkFile(context: Context) {
|
||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||
val fileCopy = java.io.File(
|
||||
context.cacheDir,
|
||||
"${`package`}-${version}.apk"
|
||||
"${componentName.packageName}-${versionName}.apk"
|
||||
)
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val user = getUser()
|
||||
val info = if (user != null) {
|
||||
launcherApps.getApplicationInfo(`package`, 0, user)
|
||||
} else {
|
||||
context.packageManager.getApplicationInfo(`package`, 0)
|
||||
}
|
||||
val info = launcherApps.getApplicationInfo(componentName.packageName, 0, user)
|
||||
val file = java.io.File(info.publicSourceDir)
|
||||
|
||||
try {
|
||||
@ -212,6 +214,13 @@ data class LauncherApp(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getActivityInfo(context: Context): ActivityInfo? {
|
||||
if (isAtLeastApiLevel(31)) {
|
||||
return launcherActivityInfo.activityInfo
|
||||
}
|
||||
return super.getActivityInfo(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun getStoreLinkForInstaller(
|
||||
installerPackage: String?,
|
||||
@ -225,18 +234,21 @@ data class LauncherApp(
|
||||
"http://www.amazon.com/gp/mas/dl/android?p=${packageName}"
|
||||
)
|
||||
}
|
||||
|
||||
"com.android.vending" -> {
|
||||
StoreLink(
|
||||
"Google Play Store",
|
||||
"https://play.google.com/store/apps/details?id=${packageName}"
|
||||
)
|
||||
}
|
||||
|
||||
"org.fdroid.fdroid", "com.aurora.adroid" -> {
|
||||
StoreLink(
|
||||
"F-Droid",
|
||||
"https://f-droid.org/packages/${packageName}"
|
||||
)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@ -251,9 +263,8 @@ data class LauncherApp(
|
||||
|
||||
const val Domain = "app"
|
||||
}
|
||||
}
|
||||
|
||||
data class StoreLink(
|
||||
val label: String,
|
||||
val url: String
|
||||
)
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return LauncherAppSerializer()
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,14 @@
|
||||
package de.mm20.launcher2.applications
|
||||
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val applicationsModule = module {
|
||||
single<AppRepository> { AppRepositoryImpl(androidContext()) }
|
||||
factory<SearchableRepository<Application>>(named<Application>()) { AppRepositoryImpl(androidContext()) }
|
||||
factory<AppRepository> { AppRepositoryImpl(androidContext()) }
|
||||
factory<SearchableDeserializer>(named(LauncherApp.Domain)) { LauncherAppDeserializer(androidContext()) }
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
@ -1,21 +1,19 @@
|
||||
package de.mm20.launcher2.appshortcuts
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherActivityInfo
|
||||
import android.content.pm.LauncherApps
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Process
|
||||
import android.os.UserHandle
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import de.mm20.launcher2.ktx.normalize
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -28,22 +26,25 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.text.similarity.FuzzyScore
|
||||
import java.util.Locale
|
||||
|
||||
interface AppShortcutRepository {
|
||||
interface AppShortcutRepository : SearchableRepository<AppShortcut> {
|
||||
|
||||
fun search(query: String): Flow<ImmutableList<AppShortcut>>
|
||||
suspend fun getShortcutsForActivity(
|
||||
launcherActivityInfo: LauncherActivityInfo,
|
||||
count: Int = 5
|
||||
): List<LauncherShortcut>
|
||||
fun findMany(
|
||||
componentName: ComponentName? = null,
|
||||
user: UserHandle = Process.myUserHandle(),
|
||||
manifest: Boolean = false,
|
||||
dynamic: Boolean = false,
|
||||
pinned: Boolean = false,
|
||||
cached: Boolean = false,
|
||||
limit: Int = 5,
|
||||
): Flow<ImmutableList<AppShortcut>>
|
||||
|
||||
suspend fun getShortcutsConfigActivities(): List<LauncherApp>
|
||||
|
||||
fun removePinnedShortcut(shortcut: LauncherShortcut)
|
||||
suspend fun getShortcutsConfigActivities(): List<AppShortcutConfigActivity>
|
||||
}
|
||||
|
||||
internal class AppShortcutRepositoryImpl(
|
||||
@ -53,33 +54,58 @@ internal class AppShortcutRepositoryImpl(
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
||||
|
||||
override suspend fun getShortcutsForActivity(
|
||||
launcherActivityInfo: LauncherActivityInfo,
|
||||
count: Int,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||
if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList()
|
||||
val query = LauncherApps.ShortcutQuery()
|
||||
.setPackage(launcherActivityInfo.applicationInfo.packageName)
|
||||
.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST)
|
||||
val shortcuts = try {
|
||||
launcherApps.getShortcuts(query, launcherActivityInfo.user)
|
||||
} catch (e: IllegalStateException) {
|
||||
emptyList()
|
||||
}
|
||||
val appShortcuts = mutableListOf<LauncherShortcut>()
|
||||
appShortcuts.addAll(shortcuts
|
||||
?.let {
|
||||
if (it.size > count) it.subList(0, count)
|
||||
else it
|
||||
}
|
||||
?.map {
|
||||
LauncherShortcut(
|
||||
context,
|
||||
it,
|
||||
override fun findMany(
|
||||
componentName: ComponentName?,
|
||||
user: UserHandle,
|
||||
manifest: Boolean,
|
||||
dynamic: Boolean,
|
||||
pinned: Boolean,
|
||||
cached: Boolean,
|
||||
limit: Int
|
||||
): Flow<ImmutableList<AppShortcut>> = flow {
|
||||
val shortcuts = withContext(Dispatchers.IO) {
|
||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
||||
if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList()
|
||||
val query = LauncherApps.ShortcutQuery()
|
||||
.setActivity(componentName)
|
||||
.setQueryFlags(
|
||||
buildQueryFlags(manifest, dynamic, pinned, cached)
|
||||
)
|
||||
} ?: emptyList())
|
||||
appShortcuts
|
||||
val shortcuts = try {
|
||||
launcherApps.getShortcuts(query, user)
|
||||
} catch (e: IllegalStateException) {
|
||||
emptyList()
|
||||
}
|
||||
val appShortcuts = mutableListOf<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>> {
|
||||
@ -93,7 +119,6 @@ internal class AppShortcutRepositoryImpl(
|
||||
return@withContext
|
||||
}
|
||||
|
||||
|
||||
shortcutChangeEmitter.collectLatest {
|
||||
val launcherApps =
|
||||
context.getSystemService<LauncherApps>() ?: return@collectLatest send(
|
||||
@ -180,39 +205,16 @@ internal class AppShortcutRepositoryImpl(
|
||||
}
|
||||
}.shareIn(scope, SharingStarted.WhileSubscribed(500), 1)
|
||||
|
||||
override fun removePinnedShortcut(shortcut: LauncherShortcut) {
|
||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
if (!launcherApps.hasShortcutHostPermission()) return
|
||||
val pinnedShortcutsQuery = LauncherApps.ShortcutQuery().apply {
|
||||
setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
|
||||
}
|
||||
val userHandle = shortcut.launcherShortcut.userHandle
|
||||
val allPinned = launcherApps.getShortcuts(pinnedShortcutsQuery, userHandle)
|
||||
|
||||
if (allPinned == null) {
|
||||
Log.e("MM20", "Could not remove shortcut ${shortcut.key}: shortcut query returned null")
|
||||
return
|
||||
}
|
||||
|
||||
launcherApps.pinShortcuts(
|
||||
shortcut.launcherShortcut.`package`,
|
||||
allPinned.filter { it.id != shortcut.launcherShortcut.id }.map { it.id },
|
||||
userHandle
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getShortcutsConfigActivities(): List<LauncherApp> {
|
||||
override suspend fun getShortcutsConfigActivities(): List<AppShortcutConfigActivity> {
|
||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
if (!launcherApps.hasShortcutHostPermission()) return emptyList()
|
||||
val results = mutableListOf<LauncherApp>()
|
||||
val results = mutableListOf<AppShortcutConfigActivity>()
|
||||
val profiles = launcherApps.profiles
|
||||
for (profile in profiles) {
|
||||
val activities = launcherApps.getShortcutConfigActivityList(null, profile)
|
||||
results.addAll(
|
||||
activities.map {
|
||||
LauncherApp(
|
||||
context, it
|
||||
)
|
||||
AppShortcutConfigActivity(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -11,9 +11,6 @@ import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||
import de.mm20.launcher2.search.data.LegacyShortcut
|
||||
import de.mm20.launcher2.search.data.UnavailableShortcut
|
||||
import org.json.JSONObject
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
@ -37,7 +34,7 @@ class LauncherShortcutDeserializer(
|
||||
val context: Context
|
||||
) : SearchableDeserializer, KoinComponent {
|
||||
|
||||
override fun deserialize(serialized: String): SavableSearchable? {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
|
||||
val json = JSONObject(serialized)
|
||||
@ -65,16 +62,9 @@ class LauncherShortcutDeserializer(
|
||||
} catch (e: IllegalStateException) {
|
||||
return null
|
||||
}
|
||||
val pm = context.packageManager
|
||||
val appName = try {
|
||||
pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString()
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return null
|
||||
}
|
||||
if (shortcuts == null || shortcuts.isEmpty()) {
|
||||
if (shortcuts.isNullOrEmpty()) {
|
||||
return null
|
||||
} else {
|
||||
val activity = shortcuts[0].activity
|
||||
return LauncherShortcut(
|
||||
context = context,
|
||||
launcherShortcut = shortcuts[0],
|
||||
@ -84,6 +74,21 @@ class LauncherShortcutDeserializer(
|
||||
}
|
||||
}
|
||||
|
||||
class UnavailableShortcutSerializer: SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String? {
|
||||
searchable as UnavailableShortcut
|
||||
return jsonObjectOf(
|
||||
"packagename" to searchable.packageName,
|
||||
"id" to searchable.shortcutId,
|
||||
"user" to searchable.userSerial,
|
||||
).toString()
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = LauncherShortcut.Domain
|
||||
|
||||
}
|
||||
|
||||
class LegacyShortcutSerializer: SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as LegacyShortcut
|
||||
@ -106,7 +111,7 @@ class LegacyShortcutSerializer: SearchableSerializer {
|
||||
class LegacyShortcutDeserializer(
|
||||
val context: Context
|
||||
): SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable {
|
||||
val json = JSONObject(serialized)
|
||||
val label = json.getString("label")
|
||||
val intent = Intent.parseUri(json.getString("intent"), 0)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.appshortcuts
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.LauncherApps
|
||||
@ -12,11 +13,13 @@ import android.os.Process
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import de.mm20.launcher2.appshortcuts.R
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.ktx.getSerialNumber
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.search.AppProfile
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.lang.NullPointerException
|
||||
@ -24,7 +27,7 @@ import java.lang.NullPointerException
|
||||
/**
|
||||
* Represents a modern (Android O+) launcher shortcut
|
||||
*/
|
||||
data class LauncherShortcut(
|
||||
internal data class LauncherShortcut(
|
||||
val launcherShortcut: ShortcutInfo,
|
||||
override val appName: String?,
|
||||
internal val userSerialNumber: Long,
|
||||
@ -32,6 +35,11 @@ data class LauncherShortcut(
|
||||
) : AppShortcut {
|
||||
|
||||
override val domain: String = Domain
|
||||
override val componentName: ComponentName?
|
||||
get() = launcherShortcut.activity
|
||||
|
||||
override val packageName: String
|
||||
get() = launcherShortcut.`package`
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
@ -48,7 +56,7 @@ data class LauncherShortcut(
|
||||
)
|
||||
|
||||
override val label: String
|
||||
get() = launcherShortcut.shortLabel?.toString() ?: ""
|
||||
get() = launcherShortcut.shortLabel?.toString() ?: launcherShortcut.longLabel?.toString() ?: ""
|
||||
|
||||
override fun overrideLabel(label: String): LauncherShortcut {
|
||||
return this.copy(labelOverride = label)
|
||||
@ -56,8 +64,10 @@ data class LauncherShortcut(
|
||||
|
||||
override val preferDetailsOverLaunch: Boolean = false
|
||||
|
||||
private val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
|
||||
override val profile: AppProfile
|
||||
get() = if (isMainProfile) AppProfile.Personal else AppProfile.Work
|
||||
|
||||
val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
|
||||
|
||||
override val key: String
|
||||
get() = if (isMainProfile) {
|
||||
@ -145,6 +155,31 @@ data class LauncherShortcut(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return LauncherShortcutSerializer()
|
||||
}
|
||||
|
||||
override suspend fun delete(context: Context) {
|
||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
if (!launcherApps.hasShortcutHostPermission()) return
|
||||
val pinnedShortcutsQuery = LauncherApps.ShortcutQuery().apply {
|
||||
setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
|
||||
}
|
||||
val userHandle = launcherShortcut.userHandle
|
||||
val allPinned = launcherApps.getShortcuts(pinnedShortcutsQuery, userHandle)
|
||||
|
||||
if (allPinned == null) {
|
||||
Log.e("MM20", "Could not remove shortcut ${key}: shortcut query returned null")
|
||||
return
|
||||
}
|
||||
|
||||
launcherApps.pinShortcuts(
|
||||
launcherShortcut.`package`,
|
||||
allPinned.filter { it.id != launcherShortcut.id }.map { it.id },
|
||||
userHandle
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromPinRequestIntent(context: Context, data: Intent): LauncherShortcut? {
|
||||
val launcherApps =
|
||||
@ -1,5 +1,6 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.appshortcuts
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ShortcutIconResource
|
||||
@ -10,8 +11,11 @@ import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.ktx.getDrawableOrNull
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.AppProfile
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
|
||||
data class LegacyShortcut(
|
||||
internal data class LegacyShortcut(
|
||||
val intent: Intent,
|
||||
override val label: String,
|
||||
override val appName: String?,
|
||||
@ -22,6 +26,9 @@ data class LegacyShortcut(
|
||||
override val domain = Domain
|
||||
override val key: String = "$domain://${intent.toUri(0)}"
|
||||
|
||||
override val profile: AppProfile
|
||||
get() = AppProfile.Personal
|
||||
|
||||
override fun overrideLabel(label: String): LegacyShortcut {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
@ -31,7 +38,10 @@ data class LegacyShortcut(
|
||||
return context.tryStartActivity(intent, options)
|
||||
}
|
||||
|
||||
val packageName: String?
|
||||
override val componentName: ComponentName?
|
||||
get() = intent.component
|
||||
|
||||
override val packageName: String?
|
||||
get() = intent.`package` ?: intent.component?.packageName
|
||||
|
||||
override suspend fun loadIcon(context: Context, size: Int, themed: Boolean): LauncherIcon? {
|
||||
@ -75,6 +85,10 @@ data class LegacyShortcut(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return LegacyShortcutSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val Domain = "legacyshortcut"
|
||||
@ -1,8 +1,15 @@
|
||||
package de.mm20.launcher2.appshortcuts
|
||||
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appShortcutsModule = module {
|
||||
single<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()) }
|
||||
}
|
||||
@ -1,24 +1,27 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.appshortcuts
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.os.UserManager
|
||||
import de.mm20.launcher2.appshortcuts.R
|
||||
import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TintedIconLayer
|
||||
import de.mm20.launcher2.search.AppProfile
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
|
||||
/**
|
||||
* Shortcut class that is used when a [LauncherShortcut] is not available, e.g. missing permissions
|
||||
* when Kvaesitso is not set as default launcher.
|
||||
*/
|
||||
class UnavailableShortcut(
|
||||
internal class UnavailableShortcut(
|
||||
override val label: String,
|
||||
override val appName: String?,
|
||||
val packageName: String,
|
||||
override val packageName: String,
|
||||
val shortcutId: String,
|
||||
val isMainProfile: Boolean,
|
||||
val userSerial: Long,
|
||||
@ -34,6 +37,8 @@ class UnavailableShortcut(
|
||||
|
||||
override val labelOverride: String?
|
||||
get() = null
|
||||
override val componentName: ComponentName?
|
||||
get() = null
|
||||
|
||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||
return StaticLauncherIcon(
|
||||
@ -56,6 +61,14 @@ class UnavailableShortcut(
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return UnavailableShortcutSerializer()
|
||||
}
|
||||
|
||||
override val isUnavailable: Boolean = true
|
||||
override val profile: AppProfile
|
||||
get() = TODO("Not yet implemented")
|
||||
|
||||
companion object {
|
||||
internal operator fun invoke(context: Context, id: String, packageName: String, userSerial: Long): UnavailableShortcut? {
|
||||
val appInfo = try {
|
||||
@ -1,4 +1,4 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.calendar
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
@ -6,50 +6,33 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TextLayer
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import java.net.URLEncoder
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
data class CalendarEvent(
|
||||
internal data class AndroidCalendarEvent(
|
||||
override val label: String,
|
||||
val id: Long,
|
||||
val color: Int,
|
||||
val startTime: Long,
|
||||
val endTime: Long,
|
||||
val allDay: Boolean,
|
||||
val location: String,
|
||||
val attendees: List<String>,
|
||||
val description: String,
|
||||
override val color: Int,
|
||||
override val startTime: Long,
|
||||
override val endTime: Long,
|
||||
override val allDay: Boolean,
|
||||
override val location: String?,
|
||||
override val attendees: List<String>,
|
||||
override val description: String?,
|
||||
val calendar: Long,
|
||||
override val labelOverride: String? = null,
|
||||
) : SavableSearchable {
|
||||
) : CalendarEvent {
|
||||
|
||||
override val domain: String = Domain
|
||||
|
||||
override val key: String
|
||||
get() = "$domain://$id"
|
||||
|
||||
override val preferDetailsOverLaunch: Boolean = true
|
||||
|
||||
override fun overrideLabel(label: String): CalendarEvent {
|
||||
override fun overrideLabel(label: String): AndroidCalendarEvent {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
|
||||
val df = SimpleDateFormat("dd")
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TextLayer(
|
||||
text = df.format(startTime),
|
||||
color = color
|
||||
),
|
||||
backgroundLayer = ColorLayer(color)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLaunchIntent(): Intent {
|
||||
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id)
|
||||
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
@ -59,7 +42,8 @@ data class CalendarEvent(
|
||||
return context.tryStartActivity(getLaunchIntent(), options)
|
||||
}
|
||||
|
||||
fun openLocation(context: Context) {
|
||||
override fun openLocation(context: Context) {
|
||||
if (location == null) return
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(
|
||||
@ -76,6 +60,10 @@ data class CalendarEvent(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return CalendarEventSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "calendar"
|
||||
}
|
||||
@ -6,8 +6,8 @@ import android.provider.CalendarContract
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import de.mm20.launcher2.search.data.UserCalendar
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -18,16 +18,16 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import java.util.Calendar
|
||||
|
||||
interface CalendarRepository {
|
||||
|
||||
fun search(query: String): Flow<ImmutableList<CalendarEvent>>
|
||||
fun getUpcomingEvents(
|
||||
excludeCalendars: List<Long>,
|
||||
excludeAllDayEvents: Boolean
|
||||
): Flow<List<CalendarEvent>>
|
||||
interface CalendarRepository: SearchableRepository<CalendarEvent> {
|
||||
fun findMany(
|
||||
from: Long = System.currentTimeMillis(),
|
||||
to: Long = from + 14 * 24 * 60 * 60 * 1000L,
|
||||
excludeCalendars: List<Long> = emptyList(),
|
||||
excludeAllDayEvents: Boolean = false,
|
||||
limit: Int = 999,
|
||||
): Flow<ImmutableList<CalendarEvent>>
|
||||
|
||||
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(
|
||||
query: String,
|
||||
query: String?,
|
||||
intervalStart: Long,
|
||||
intervalEnd: Long,
|
||||
limit: Int = 10,
|
||||
excludeAllDayEvents: Boolean = false,
|
||||
excludeCalendars: List<Long> = emptyList(),
|
||||
): List<CalendarEvent> {
|
||||
): List<AndroidCalendarEvent> {
|
||||
val results = withContext(Dispatchers.IO) {
|
||||
val results = mutableListOf<CalendarEvent>()
|
||||
val results = mutableListOf<AndroidCalendarEvent>()
|
||||
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
|
||||
ContentUris.appendId(builder, intervalStart)
|
||||
ContentUris.appendId(builder, intervalEnd)
|
||||
@ -86,10 +113,10 @@ internal class CalendarRepositoryImpl(
|
||||
CalendarContract.Instances.DESCRIPTION
|
||||
)
|
||||
val selection = mutableListOf<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 (excludeAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0")
|
||||
val selArgs = if (query.isBlank()) null else arrayOf("%$query%")
|
||||
val selArgs = if (query != null) null else arrayOf("%$query%")
|
||||
val sort =
|
||||
"${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else ""
|
||||
val cursor = context.contentResolver.query(
|
||||
@ -128,7 +155,7 @@ internal class CalendarRepositoryImpl(
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val event = CalendarEvent(
|
||||
val event = AndroidCalendarEvent(
|
||||
label = cursor.getStringOrNull(1) ?: "",
|
||||
id = cursor.getLong(0),
|
||||
color = cursor.getInt(5),
|
||||
@ -150,32 +177,6 @@ internal class CalendarRepositoryImpl(
|
||||
return results
|
||||
}
|
||||
|
||||
override fun getUpcomingEvents(
|
||||
excludeCalendars: List<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> {
|
||||
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Calendar)) return emptyList()
|
||||
return withContext(Dispatchers.IO) {
|
||||
|
||||
@ -10,13 +10,12 @@ import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
class CalendarEventSerializer: SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as CalendarEvent
|
||||
searchable as AndroidCalendarEvent
|
||||
val json = JSONObject()
|
||||
json.put("id", searchable.id)
|
||||
return json.toString()
|
||||
@ -27,7 +26,7 @@ class CalendarEventSerializer: SearchableSerializer {
|
||||
}
|
||||
|
||||
class CalendarEventDeserializer(val context: Context): SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable? {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
|
||||
val json = JSONObject(serialized)
|
||||
val id = json.getLong("id")
|
||||
@ -86,7 +85,7 @@ class CalendarEventDeserializer(val context: Context): SearchableDeserializer {
|
||||
} else {
|
||||
0
|
||||
}
|
||||
return CalendarEvent(
|
||||
return AndroidCalendarEvent(
|
||||
label = title,
|
||||
id = id,
|
||||
color = color,
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val calendarModule = module {
|
||||
single<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get()) }
|
||||
factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { CalendarRepositoryImpl(androidContext(), get()) }
|
||||
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get()) }
|
||||
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { CalendarEventDeserializer(androidContext()) }
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -2,9 +2,12 @@ package de.mm20.launcher2.contacts
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.ContactInfo
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -12,14 +15,143 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface ContactRepository {
|
||||
fun search(query: String): Flow<ImmutableList<Contact>>
|
||||
}
|
||||
|
||||
internal class ContactRepositoryImpl(
|
||||
internal class ContactRepository(
|
||||
private val context: Context,
|
||||
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>> {
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
|
||||
@ -45,7 +177,8 @@ internal class ContactRepositoryImpl(
|
||||
ContactsContract.RawContacts.CONTACT_ID,
|
||||
ContactsContract.RawContacts._ID
|
||||
)
|
||||
val sel = "${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ? OR ${ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE} LIKE ? OR ${ContactsContract.RawContacts.PHONETIC_NAME} LIKE ?"
|
||||
val sel =
|
||||
"${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ? OR ${ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE} LIKE ? OR ${ContactsContract.RawContacts.PHONETIC_NAME} LIKE ?"
|
||||
val selArgs = arrayOf("%$query%", "%$query%", "%$query%")
|
||||
val cursor = context.contentResolver.query(
|
||||
ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null
|
||||
@ -58,7 +191,7 @@ internal class ContactRepositoryImpl(
|
||||
cursor.close()
|
||||
val results = mutableListOf<Contact>()
|
||||
for ((id, rawIds) in contactMap) {
|
||||
Contact.contactById(context, id, rawIds)?.let { results.add(it) }
|
||||
getWithRawIds(id, rawIds)?.let { results.add(it) }
|
||||
if (results.size > 15) break
|
||||
}
|
||||
results
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.json.JSONObject
|
||||
|
||||
class ContactSerializer : SearchableSerializer {
|
||||
internal class ContactSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as Contact
|
||||
searchable as AndroidContact
|
||||
return jsonObjectOf(
|
||||
"id" to searchable.id
|
||||
).toString()
|
||||
@ -24,28 +21,15 @@ class ContactSerializer : SearchableSerializer {
|
||||
get() = "contact"
|
||||
}
|
||||
|
||||
class ContactDeserializer(val context: Context) : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable? {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_CONTACTS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) return null
|
||||
val id = JSONObject(serialized).getLong("id")
|
||||
val rawContactsCursor = context.contentResolver.query(
|
||||
ContactsContract.RawContacts.CONTENT_URI,
|
||||
arrayOf(ContactsContract.RawContacts._ID),
|
||||
"${ContactsContract.RawContacts.CONTACT_ID} = ?",
|
||||
arrayOf(id.toString()),
|
||||
null
|
||||
) ?: return null
|
||||
val rawContacts = mutableSetOf<Long>()
|
||||
while (rawContactsCursor.moveToNext()) {
|
||||
rawContacts.add(rawContactsCursor.getLong(0))
|
||||
}
|
||||
rawContactsCursor.close()
|
||||
if (rawContacts.isEmpty()) return null
|
||||
internal class ContactDeserializer(
|
||||
private val contactRepository: ContactRepository,
|
||||
private val permissionsManager: PermissionsManager
|
||||
) : SearchableDeserializer {
|
||||
|
||||
return Contact.contactById(context, id, rawContacts)
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Contacts)) return null
|
||||
val id = JSONObject(serialized).getLong("id")
|
||||
|
||||
return contactRepository.get(id).first()
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,14 @@
|
||||
package de.mm20.launcher2.contacts
|
||||
|
||||
import de.mm20.launcher2.search.Contact
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val contactsModule = module {
|
||||
single<ContactRepository> { ContactRepositoryImpl(androidContext(), get()) }
|
||||
factory { ContactRepository(androidContext(), get()) }
|
||||
factory<SearchableRepository<Contact>>(named<Contact>()) { ContactRepository(androidContext(), get()) }
|
||||
factory<SearchableDeserializer>(named(AndroidContact.Domain)) { ContactDeserializer(get(), get()) }
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
@ -1,10 +1,9 @@
|
||||
package de.mm20.launcher2.data.customattrs
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@ -48,7 +47,7 @@ interface CustomAttributesRepository {
|
||||
|
||||
internal class CustomAttributesRepositoryImpl(
|
||||
private val appDatabase: AppDatabase,
|
||||
private val searchableRepository: SearchableRepository
|
||||
private val searchableRepository: SavableSearchableRepository
|
||||
) : CustomAttributesRepository {
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
|
||||
@ -51,4 +51,5 @@ dependencies {
|
||||
implementation(project(":core:i18n"))
|
||||
implementation(project(":core:permissions"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
implementation(project(":core:preferences"))
|
||||
}
|
||||
@ -3,18 +3,22 @@ package de.mm20.launcher2.files
|
||||
import android.content.Context
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.files.providers.GDriveFile
|
||||
import de.mm20.launcher2.files.providers.LocalFile
|
||||
import de.mm20.launcher2.files.providers.NextcloudFile
|
||||
import de.mm20.launcher2.files.providers.OneDriveFile
|
||||
import de.mm20.launcher2.files.providers.OwncloudFile
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.data.*
|
||||
import org.json.JSONObject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
class LocalFileSerializer : SearchableSerializer {
|
||||
internal class LocalFileSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as LocalFile
|
||||
return jsonObjectOf(
|
||||
@ -26,10 +30,10 @@ class LocalFileSerializer : SearchableSerializer {
|
||||
get() = "file"
|
||||
}
|
||||
|
||||
class LocalFileDeserializer(
|
||||
internal class LocalFileDeserializer(
|
||||
val context: Context
|
||||
) : SearchableDeserializer, KoinComponent {
|
||||
override fun deserialize(serialized: String): SavableSearchable? {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
val permissionsManager: PermissionsManager = get()
|
||||
if (!permissionsManager.checkPermissionOnce(
|
||||
PermissionGroup.ExternalStorage
|
||||
@ -73,7 +77,7 @@ class LocalFileDeserializer(
|
||||
}
|
||||
}
|
||||
|
||||
class GDriveFileSerializer : SearchableSerializer {
|
||||
internal class GDriveFileSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as GDriveFile
|
||||
return jsonObjectOf(
|
||||
@ -102,8 +106,8 @@ class GDriveFileSerializer : SearchableSerializer {
|
||||
get() = "gdrive"
|
||||
}
|
||||
|
||||
class GDriveFileDeserializer : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable {
|
||||
internal class GDriveFileDeserializer : SearchableDeserializer {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable {
|
||||
val json = JSONObject(serialized)
|
||||
val id = json.getString("id")
|
||||
val label = json.getString("label")
|
||||
@ -133,7 +137,7 @@ class GDriveFileDeserializer : SearchableDeserializer {
|
||||
}
|
||||
}
|
||||
|
||||
class OneDriveFileSerializer : SearchableSerializer {
|
||||
internal class OneDriveFileSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as OneDriveFile
|
||||
return jsonObjectOf(
|
||||
@ -160,8 +164,8 @@ class OneDriveFileSerializer : SearchableSerializer {
|
||||
get() = "onedrive"
|
||||
}
|
||||
|
||||
class OneDriveFileDeserializer : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable {
|
||||
internal class OneDriveFileDeserializer : SearchableDeserializer {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable {
|
||||
val json = JSONObject(serialized)
|
||||
val fileId = json.getString("id")
|
||||
val label = json.getString("label")
|
||||
@ -188,7 +192,7 @@ class OneDriveFileDeserializer : SearchableDeserializer {
|
||||
}
|
||||
}
|
||||
|
||||
class NextcloudFileSerializer : SearchableSerializer {
|
||||
internal class NextcloudFileSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as NextcloudFile
|
||||
return jsonObjectOf(
|
||||
@ -215,8 +219,8 @@ class NextcloudFileSerializer : SearchableSerializer {
|
||||
get() = "nextcloud"
|
||||
}
|
||||
|
||||
class NextcloudFileDeserializer : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable {
|
||||
internal class NextcloudFileDeserializer : SearchableDeserializer {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable {
|
||||
val json = JSONObject(serialized)
|
||||
val id = json.getLong("id")
|
||||
val label = json.getString("label")
|
||||
@ -241,7 +245,7 @@ class NextcloudFileDeserializer : SearchableDeserializer {
|
||||
}
|
||||
}
|
||||
|
||||
class OwncloudFileSerializer : SearchableSerializer {
|
||||
internal class OwncloudFileSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as OwncloudFile
|
||||
return jsonObjectOf(
|
||||
@ -268,8 +272,8 @@ class OwncloudFileSerializer : SearchableSerializer {
|
||||
get() = "owncloud"
|
||||
}
|
||||
|
||||
class OwncloudFileDeserializer : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable {
|
||||
internal class OwncloudFileDeserializer : SearchableDeserializer {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable {
|
||||
val json = JSONObject(serialized)
|
||||
val id = json.getLong("id")
|
||||
val label = json.getString("label")
|
||||
|
||||
@ -9,34 +9,23 @@ import de.mm20.launcher2.files.providers.OwncloudFileProvider
|
||||
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface FileRepository {
|
||||
fun search(
|
||||
query: String,
|
||||
local: Boolean = true,
|
||||
gdrive: Boolean = true,
|
||||
onedrive: Boolean = true,
|
||||
nextcloud: Boolean = true,
|
||||
owncloud: Boolean = true,
|
||||
): Flow<ImmutableList<File>>
|
||||
|
||||
fun deleteFile(file: File)
|
||||
}
|
||||
|
||||
internal class FileRepositoryImpl(
|
||||
internal class FileRepository(
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager,
|
||||
) : FileRepository {
|
||||
private val dataStore: LauncherDataStore,
|
||||
) : SearchableRepository<File> {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
@ -49,39 +38,28 @@ internal class FileRepositoryImpl(
|
||||
|
||||
override fun search(
|
||||
query: String,
|
||||
local: Boolean,
|
||||
gdrive: Boolean,
|
||||
onedrive: Boolean,
|
||||
nextcloud: Boolean,
|
||||
owncloud: Boolean
|
||||
) = channelFlow {
|
||||
if (query.isBlank()) {
|
||||
send(persistentListOf())
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
val providers = mutableListOf<FileProvider>()
|
||||
dataStore.data.map { it.fileSearch }.collectLatest {
|
||||
val providers = mutableListOf<FileProvider>()
|
||||
|
||||
if (local) providers.add(LocalFileProvider(context, permissionsManager))
|
||||
if (gdrive) providers.add(GDriveFileProvider(context))
|
||||
if (nextcloud) providers.add(NextcloudFileProvider(nextcloudClient))
|
||||
if (owncloud) providers.add(OwncloudFileProvider(owncloudClient))
|
||||
if (it.localFiles) providers.add(LocalFileProvider(context, permissionsManager))
|
||||
if (it.gdrive) providers.add(GDriveFileProvider(context))
|
||||
if (it.nextcloud) providers.add(NextcloudFileProvider(nextcloudClient))
|
||||
if (it.owncloud) providers.add(OwncloudFileProvider(owncloudClient))
|
||||
|
||||
if (providers.isEmpty()) {
|
||||
send(persistentListOf())
|
||||
return@channelFlow
|
||||
}
|
||||
val results = mutableListOf<File>()
|
||||
for (provider in providers) {
|
||||
results.addAll(provider.search(query))
|
||||
send(results.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteFile(file: File) {
|
||||
scope.launch {
|
||||
if (file.isDeletable) {
|
||||
file.delete(context)
|
||||
if (providers.isEmpty()) {
|
||||
send(persistentListOf())
|
||||
return@collectLatest
|
||||
}
|
||||
val results = mutableListOf<File>()
|
||||
for (provider in providers) {
|
||||
results.addAll(provider.search(query))
|
||||
send(results.toImmutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,22 @@
|
||||
package de.mm20.launcher2.files
|
||||
|
||||
import de.mm20.launcher2.files.providers.GDriveFile
|
||||
import de.mm20.launcher2.files.providers.LocalFile
|
||||
import de.mm20.launcher2.files.providers.NextcloudFile
|
||||
import de.mm20.launcher2.files.providers.OneDriveFile
|
||||
import de.mm20.launcher2.files.providers.OwncloudFile
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val filesModule = module {
|
||||
single<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() }
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package de.mm20.launcher2.files.providers
|
||||
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.File
|
||||
|
||||
interface FileProvider {
|
||||
internal interface FileProvider {
|
||||
suspend fun search(query: String): List<File>
|
||||
}
|
||||
@ -1,13 +1,16 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.files.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.files.GDriveFileSerializer
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
|
||||
data class GDriveFile(
|
||||
internal data class GDriveFile(
|
||||
val fileId: String,
|
||||
override val label: String,
|
||||
override val path: String,
|
||||
@ -43,6 +46,10 @@ data class GDriveFile(
|
||||
return context.tryStartActivity(getLaunchIntent(), options)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return GDriveFileSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "gdrive"
|
||||
}
|
||||
@ -4,8 +4,7 @@ import android.content.Context
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.gservices.DriveFileMeta
|
||||
import de.mm20.launcher2.gservices.GoogleApiHelper
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.GDriveFile
|
||||
import de.mm20.launcher2.search.File
|
||||
|
||||
internal class GDriveFileProvider(
|
||||
private val context: Context
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.files.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -16,17 +16,21 @@ import android.util.Size
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.files.LocalFileSerializer
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.ktx.formatToString
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.media.ThumbnailUtilsCompat
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.io.File as JavaIOFile
|
||||
|
||||
data class LocalFile(
|
||||
internal data class LocalFile(
|
||||
val id: Long,
|
||||
override val path: String,
|
||||
override val mimeType: String,
|
||||
@ -186,7 +190,7 @@ data class LocalFile(
|
||||
|
||||
val file = java.io.File(path)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
withContext(NonCancellable + Dispatchers.IO) {
|
||||
file.deleteRecursively()
|
||||
|
||||
context.contentResolver.delete(
|
||||
@ -352,4 +356,8 @@ data class LocalFile(
|
||||
shareIntent.type = mimeType
|
||||
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return LocalFileSerializer()
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,11 @@
|
||||
package de.mm20.launcher2.files.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.LocalFile
|
||||
import de.mm20.launcher2.search.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.files.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.files.NextcloudFileSerializer
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
|
||||
data class NextcloudFile(
|
||||
internal data class NextcloudFile(
|
||||
val fileId: Long,
|
||||
override val label: String,
|
||||
override val path: String,
|
||||
@ -45,6 +48,10 @@ data class NextcloudFile(
|
||||
return context.tryStartActivity(getLaunchIntent(context), options)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return NextcloudFileSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val Domain = "nextcloud"
|
||||
@ -2,8 +2,7 @@ package de.mm20.launcher2.files.providers
|
||||
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.NextcloudFile
|
||||
import de.mm20.launcher2.search.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.min
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.files.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.files.OneDriveFileSerializer
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
|
||||
data class OneDriveFile(
|
||||
internal data class OneDriveFile(
|
||||
val fileId: String,
|
||||
override val label: String,
|
||||
override val path: String,
|
||||
@ -42,6 +45,10 @@ data class OneDriveFile(
|
||||
return context.tryStartActivity(getLaunchIntent(), options)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return OneDriveFileSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "onedrive"
|
||||
}
|
||||
@ -1,13 +1,16 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.files.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.files.OwncloudFileSerializer
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
|
||||
data class OwncloudFile(
|
||||
internal data class OwncloudFile(
|
||||
val fileId: Long,
|
||||
override val label: String,
|
||||
override val path: String,
|
||||
@ -42,6 +45,10 @@ data class OwncloudFile(
|
||||
return context.tryStartActivity(getLaunchIntent(), options)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return OwncloudFileSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "owncloud"
|
||||
}
|
||||
@ -2,8 +2,7 @@ package de.mm20.launcher2.files.providers
|
||||
|
||||
import de.mm20.launcher2.files.R
|
||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.OwncloudFile
|
||||
import de.mm20.launcher2.search.File
|
||||
|
||||
internal class OwncloudFileProvider(
|
||||
private val owncloudClient: OwncloudClient
|
||||
|
||||
@ -45,12 +45,7 @@ dependencies {
|
||||
implementation(project(":data:calendar"))
|
||||
implementation(project(":core:database"))
|
||||
implementation(project(":core:preferences"))
|
||||
implementation(project(":data:applications"))
|
||||
implementation(project(":data:appshortcuts"))
|
||||
implementation(project(":data:contacts"))
|
||||
implementation(project(":core:ktx"))
|
||||
implementation(project(":data:files"))
|
||||
implementation(project(":data:websites"))
|
||||
implementation(project(":data:wikipedia"))
|
||||
implementation(project(":services:badges"))
|
||||
implementation(project(":core:crashreporter"))
|
||||
|
||||
@ -6,6 +6,8 @@ import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TextLayer
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.searchable.TagSerializer
|
||||
|
||||
data class Tag(
|
||||
val tag: String,
|
||||
@ -33,6 +35,10 @@ data class Tag(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return TagSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "tag"
|
||||
}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
package de.mm20.launcher2.searchable
|
||||
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.data.Tag
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val searchableModule = module {
|
||||
single<SearchableRepository> { SearchableRepositoryImpl(androidContext(), get(), get()) }
|
||||
factory <SavableSearchableRepository> { SavableSearchableRepositoryImpl(androidContext(), get(), get()) }
|
||||
factory<SearchableDeserializer>(named(Tag.Domain)) { TagDeserializer() }
|
||||
}
|
||||
@ -24,9 +24,13 @@ import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.error.InstanceCreationException
|
||||
import org.koin.core.error.NoBeanDefFoundException
|
||||
import org.koin.core.qualifier.named
|
||||
import java.io.File
|
||||
|
||||
interface SearchableRepository {
|
||||
interface SavableSearchableRepository {
|
||||
|
||||
fun insert(
|
||||
searchable: SavableSearchable,
|
||||
@ -114,11 +118,11 @@ interface SearchableRepository {
|
||||
suspend fun cleanupDatabase(): Int
|
||||
}
|
||||
|
||||
internal class SearchableRepositoryImpl(
|
||||
internal class SavableSearchableRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val database: AppDatabase,
|
||||
private val dataStore: LauncherDataStore
|
||||
) : SearchableRepository, KoinComponent {
|
||||
) : SavableSearchableRepository, KoinComponent {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
@ -348,10 +352,17 @@ internal class SearchableRepositoryImpl(
|
||||
return database.searchableDao().sortByWeight(keys)
|
||||
}
|
||||
|
||||
private fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable {
|
||||
val deserializer: SearchableDeserializer =
|
||||
getDeserializer(context, entity.type)
|
||||
val searchable = deserializer.deserialize(entity.serializedSearchable)
|
||||
private suspend fun fromDatabaseEntity(entity: SavedSearchableEntity): SavedSearchable {
|
||||
val deserializer: SearchableDeserializer? = try {
|
||||
get(named(entity.type))
|
||||
} catch (e: NoBeanDefFoundException) {
|
||||
CrashReporter.logException(e)
|
||||
null
|
||||
} catch (e: InstanceCreationException) {
|
||||
CrashReporter.logException(e)
|
||||
null
|
||||
}
|
||||
val searchable = deserializer?.deserialize(entity.serializedSearchable)
|
||||
if (searchable == null) removeInvalidItem(entity.key)
|
||||
return SavedSearchable(
|
||||
key = entity.key,
|
||||
@ -15,9 +15,7 @@ data class SavedSearchable(
|
||||
var weight: Double
|
||||
) {
|
||||
fun toDatabaseEntity(): SavedSearchableEntity? {
|
||||
val serializer = getSerializer(searchable)
|
||||
|
||||
val data = searchable?.let { serializer.serialize(it) } ?: return null
|
||||
val data = searchable?.serialize() ?: return null
|
||||
|
||||
return SavedSearchableEntity(
|
||||
key = key,
|
||||
|
||||
@ -1,114 +1,8 @@
|
||||
package de.mm20.launcher2.searchable
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.appshortcuts.LauncherShortcutDeserializer
|
||||
import de.mm20.launcher2.appshortcuts.LauncherShortcutSerializer
|
||||
import de.mm20.launcher2.appshortcuts.LegacyShortcutDeserializer
|
||||
import de.mm20.launcher2.appshortcuts.LegacyShortcutSerializer
|
||||
import de.mm20.launcher2.calendar.CalendarEventDeserializer
|
||||
import de.mm20.launcher2.calendar.CalendarEventSerializer
|
||||
import de.mm20.launcher2.contacts.ContactDeserializer
|
||||
import de.mm20.launcher2.contacts.ContactSerializer
|
||||
import de.mm20.launcher2.files.*
|
||||
import de.mm20.launcher2.search.NullDeserializer
|
||||
import de.mm20.launcher2.search.NullSerializer
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.data.*
|
||||
import de.mm20.launcher2.websites.WebsiteDeserializer
|
||||
import de.mm20.launcher2.websites.WebsiteSerializer
|
||||
import de.mm20.launcher2.wikipedia.WikipediaDeserializer
|
||||
import de.mm20.launcher2.wikipedia.WikipediaSerializer
|
||||
|
||||
internal fun SavableSearchable.serialize(): String? {
|
||||
val serializer = getSerializer(this)
|
||||
val serializer = getSerializer()
|
||||
return serializer.serialize(this)
|
||||
}
|
||||
|
||||
internal fun getSerializer(searchable: Searchable?): SearchableSerializer {
|
||||
if (searchable is LauncherApp) {
|
||||
return LauncherAppSerializer()
|
||||
}
|
||||
if (searchable is LauncherShortcut) {
|
||||
return LauncherShortcutSerializer()
|
||||
}
|
||||
if (searchable is LegacyShortcut) {
|
||||
return LegacyShortcutSerializer()
|
||||
}
|
||||
if (searchable is CalendarEvent) {
|
||||
return CalendarEventSerializer()
|
||||
}
|
||||
if (searchable is Contact) {
|
||||
return ContactSerializer()
|
||||
}
|
||||
if (searchable is Wikipedia) {
|
||||
return WikipediaSerializer()
|
||||
}
|
||||
if (searchable is GDriveFile) {
|
||||
return GDriveFileSerializer()
|
||||
}
|
||||
if (searchable is OneDriveFile) {
|
||||
return OneDriveFileSerializer()
|
||||
}
|
||||
if (searchable is OwncloudFile) {
|
||||
return OwncloudFileSerializer()
|
||||
}
|
||||
if (searchable is NextcloudFile) {
|
||||
return NextcloudFileSerializer()
|
||||
}
|
||||
if (searchable is LocalFile) {
|
||||
return LocalFileSerializer()
|
||||
}
|
||||
if (searchable is Website) {
|
||||
return WebsiteSerializer()
|
||||
}
|
||||
if (searchable is Tag) {
|
||||
return TagSerializer()
|
||||
}
|
||||
return NullSerializer()
|
||||
}
|
||||
|
||||
internal fun getDeserializer(context: Context, type: String): SearchableDeserializer {
|
||||
if (type == LauncherApp.Domain) {
|
||||
return LauncherAppDeserializer(context)
|
||||
}
|
||||
if (type == LauncherShortcut.Domain) {
|
||||
return LauncherShortcutDeserializer(context)
|
||||
}
|
||||
if (type == LegacyShortcut.Domain) {
|
||||
return LegacyShortcutDeserializer(context)
|
||||
}
|
||||
if (type == CalendarEvent.Domain) {
|
||||
return CalendarEventDeserializer(context)
|
||||
}
|
||||
if (type == Contact.Domain) {
|
||||
return ContactDeserializer(context)
|
||||
}
|
||||
if (type == Wikipedia.Domain) {
|
||||
return WikipediaDeserializer(context)
|
||||
}
|
||||
if (type == GDriveFile.Domain) {
|
||||
return GDriveFileDeserializer()
|
||||
}
|
||||
if (type == OneDriveFile.Domain) {
|
||||
return OneDriveFileDeserializer()
|
||||
}
|
||||
if (type == NextcloudFile.Domain) {
|
||||
return NextcloudFileDeserializer()
|
||||
}
|
||||
if (type == OwncloudFile.Domain) {
|
||||
return OwncloudFileDeserializer()
|
||||
}
|
||||
if (type == LocalFile.Domain) {
|
||||
return LocalFileDeserializer(context)
|
||||
}
|
||||
if (type == Website.Domain) {
|
||||
return WebsiteDeserializer()
|
||||
}
|
||||
if (type == Tag.Domain) {
|
||||
return TagDeserializer()
|
||||
}
|
||||
return NullDeserializer()
|
||||
}
|
||||
@ -19,7 +19,7 @@ class TagSerializer: SearchableSerializer {
|
||||
}
|
||||
|
||||
class TagDeserializer: SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable {
|
||||
val json = JSONObject(serialized)
|
||||
|
||||
return Tag(json.getString("tag"))
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
package de.mm20.launcher2.websites
|
||||
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import de.mm20.launcher2.search.Website
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val websitesModule = module {
|
||||
single<WebsiteRepository> { WebsiteRepositoryImpl(androidContext()) }
|
||||
single<SearchableRepository<Website>>(named<Website>()) { WebsiteRepository(androidContext()) }
|
||||
factory<SearchableDeserializer>(named(WebsiteImpl.Domain)) { WebsiteDeserializer() }
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.websites
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -9,19 +9,19 @@ import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.websites.R
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.Website
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
data class Website(
|
||||
internal data class WebsiteImpl(
|
||||
override val label: String,
|
||||
val url: String,
|
||||
val description: String,
|
||||
val image: String,
|
||||
val favicon: String,
|
||||
val color: Int,
|
||||
override val url: String,
|
||||
override val description: String,
|
||||
override val imageUrl: String,
|
||||
override val faviconUrl: String,
|
||||
override val color: Int,
|
||||
override val labelOverride: String? = null,
|
||||
) : SavableSearchable {
|
||||
) : Website {
|
||||
|
||||
override val domain: String = Domain
|
||||
|
||||
@ -38,10 +38,10 @@ data class Website(
|
||||
size: Int,
|
||||
themed: Boolean,
|
||||
): LauncherIcon? {
|
||||
if (favicon.isEmpty()) return null
|
||||
if (faviconUrl.isEmpty()) return null
|
||||
try {
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(favicon)
|
||||
.data(faviconUrl)
|
||||
.size(size)
|
||||
.allowHardware(false)
|
||||
.build()
|
||||
@ -90,7 +90,9 @@ data class Website(
|
||||
return context.tryStartActivity(getLaunchIntent(), options)
|
||||
}
|
||||
|
||||
fun share(context: Context) {
|
||||
override val canShare: Boolean = true
|
||||
|
||||
override fun share(context: Context) {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
@ -100,6 +102,10 @@ data class Website(
|
||||
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return WebsiteSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "web"
|
||||
}
|
||||
@ -2,8 +2,12 @@ package de.mm20.launcher2.websites
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.URLUtil
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.core.graphics.toColorInt
|
||||
import de.mm20.launcher2.search.data.Website
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import de.mm20.launcher2.search.Website
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
@ -20,11 +24,8 @@ import java.net.URISyntaxException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface WebsiteRepository {
|
||||
fun search(query: String): Flow<Website?>
|
||||
}
|
||||
|
||||
internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository, KoinComponent {
|
||||
internal class WebsiteRepository(val context: Context) : SearchableRepository<Website> {
|
||||
|
||||
private val httpClient = OkHttpClient
|
||||
.Builder()
|
||||
@ -33,16 +34,17 @@ internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository,
|
||||
.writeTimeout(1000, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
override fun search(query: String): Flow<Website?> = channelFlow {
|
||||
send(null)
|
||||
override fun search(query: String): Flow<ImmutableList<Website>> = channelFlow {
|
||||
send(persistentListOf())
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.dispatcher.cancelAll()
|
||||
}
|
||||
if (query.isBlank()) return@channelFlow
|
||||
|
||||
val website = queryWebsite(query)
|
||||
send(website)
|
||||
|
||||
website?.let {
|
||||
send(persistentListOf(it))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun queryWebsite(query: String): Website? {
|
||||
@ -84,12 +86,12 @@ internal class WebsiteRepositoryImpl(val context: Context) : WebsiteRepository,
|
||||
doc.head().select("link[href~=.*\\.(ico|png)]").attr("href")
|
||||
if (favicon.isNotBlank()) favicon = resolveUrl(response.request.url, favicon)
|
||||
if (image.isNotBlank()) image = resolveUrl(response.request.url, image)
|
||||
return@withContext Website(
|
||||
return@withContext WebsiteImpl(
|
||||
label = title,
|
||||
url = url,
|
||||
description = description,
|
||||
image = image,
|
||||
favicon = favicon,
|
||||
imageUrl = image,
|
||||
faviconUrl = favicon,
|
||||
color = color
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
|
||||
@ -4,18 +4,17 @@ import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.data.Website
|
||||
import org.json.JSONObject
|
||||
|
||||
class WebsiteSerializer : SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as Website
|
||||
searchable as WebsiteImpl
|
||||
return jsonObjectOf(
|
||||
"label" to searchable.label,
|
||||
"url" to searchable.url,
|
||||
"description" to searchable.description,
|
||||
"image" to searchable.image,
|
||||
"favicon" to searchable.favicon,
|
||||
"image" to searchable.imageUrl,
|
||||
"favicon" to searchable.faviconUrl,
|
||||
"color" to searchable.color
|
||||
).toString()
|
||||
}
|
||||
@ -25,12 +24,12 @@ class WebsiteSerializer : SearchableSerializer {
|
||||
}
|
||||
|
||||
class WebsiteDeserializer: SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable? {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
val json = JSONObject(serialized)
|
||||
return Website(
|
||||
return WebsiteImpl(
|
||||
label = json.getString("label"),
|
||||
favicon = json.getString("favicon"),
|
||||
image = json.getString("image"),
|
||||
faviconUrl = json.getString("favicon"),
|
||||
imageUrl = json.getString("image"),
|
||||
description = json.getString("description"),
|
||||
url = json.getString("url"),
|
||||
color = json.getInt("color")
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
package de.mm20.launcher2.wikipedia
|
||||
|
||||
import de.mm20.launcher2.search.Article
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val wikipediaModule = module {
|
||||
single<WikipediaRepository> { WikipediaRepositoryImpl(androidContext(), get()) }
|
||||
single<SearchableRepository<Article>>(named<Article>()) { WikipediaRepository(androidContext(), get()) }
|
||||
factory<SearchableDeserializer>(named(Wikipedia.Domain)) { WikipediaDeserializer(androidContext()) }
|
||||
}
|
||||
@ -1,34 +1,31 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
package de.mm20.launcher2.wikipedia
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.HtmlCompat
|
||||
import de.mm20.launcher2.icons.ColorLayer
|
||||
import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TintedIconLayer
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.wikipedia.R
|
||||
import de.mm20.launcher2.search.Article
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
|
||||
data class Wikipedia(
|
||||
internal data class Wikipedia(
|
||||
override val label: String,
|
||||
val id: Long,
|
||||
val text: String,
|
||||
val image: String?,
|
||||
val url: String,
|
||||
override val text: String,
|
||||
override val imageUrl: String?,
|
||||
override val sourceUrl: String,
|
||||
override val sourceName: String,
|
||||
val wikipediaUrl: String,
|
||||
override val labelOverride: String? = null,
|
||||
) : SavableSearchable {
|
||||
) : Article {
|
||||
|
||||
override val domain: String = Domain
|
||||
|
||||
override val preferDetailsOverLaunch: Boolean = false
|
||||
|
||||
override fun overrideLabel(label: String): Wikipedia {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
@ -47,25 +44,30 @@ data class Wikipedia(
|
||||
}
|
||||
|
||||
private fun getLaunchIntent(): Intent {
|
||||
return Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
return Intent(Intent.ACTION_VIEW, Uri.parse(sourceUrl))
|
||||
}
|
||||
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
return context.tryStartActivity(getLaunchIntent(), options)
|
||||
}
|
||||
|
||||
fun share(context: Context) {
|
||||
override val canShare: Boolean = true
|
||||
override fun share(context: Context) {
|
||||
val text = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_TEXT, "${label}\n\n" +
|
||||
"${text.substring(0, 200)}…\n\n" +
|
||||
url
|
||||
sourceUrl
|
||||
)
|
||||
shareIntent.type = "text/plain"
|
||||
context.startActivity(Intent.createChooser(shareIntent, null))
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return WikipediaSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "wikipedia"
|
||||
}
|
||||
@ -3,7 +3,10 @@ package de.mm20.launcher2.wikipedia
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.data.Wikipedia
|
||||
import de.mm20.launcher2.search.Article
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import okhttp3.OkHttpClient
|
||||
@ -12,14 +15,11 @@ import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface WikipediaRepository {
|
||||
fun search(query: String, loadImages: Boolean = false): Flow<Wikipedia?>
|
||||
}
|
||||
|
||||
internal class WikipediaRepositoryImpl(
|
||||
internal class WikipediaRepository(
|
||||
private val context: Context,
|
||||
private val dataStore: LauncherDataStore
|
||||
) : WikipediaRepository, KoinComponent {
|
||||
) : SearchableRepository<Article> {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
@ -55,8 +55,8 @@ internal class WikipediaRepositoryImpl(
|
||||
private lateinit var wikipediaService: WikipediaApi
|
||||
|
||||
|
||||
override fun search(query: String, loadImages: Boolean): Flow<Wikipedia?> = channelFlow {
|
||||
send(null)
|
||||
override fun search(query: String): Flow<ImmutableList<Wikipedia>> = channelFlow {
|
||||
send(persistentListOf())
|
||||
withContext(Dispatchers.IO) {
|
||||
httpClient.dispatcher.cancelAll()
|
||||
}
|
||||
@ -66,7 +66,10 @@ internal class WikipediaRepositoryImpl(
|
||||
if (!::wikipediaService.isInitialized) return@channelFlow
|
||||
if (query.isBlank()) return@channelFlow
|
||||
|
||||
send(queryWikipedia(query, loadImages))
|
||||
dataStore.data.map { it.wikipediaSearch.images }.collectLatest {
|
||||
val wikipedia = queryWikipedia(query, false)
|
||||
send(wikipedia?.let { persistentListOf(it) } ?: persistentListOf())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun queryWikipedia(query: String, loadImages: Boolean): Wikipedia? {
|
||||
@ -92,9 +95,10 @@ internal class WikipediaRepositoryImpl(
|
||||
label = page.title,
|
||||
id = page.pageid,
|
||||
text = page.extract,
|
||||
image = image,
|
||||
url = page.fullurl,
|
||||
wikipediaUrl = wikipediaUrl
|
||||
imageUrl = image,
|
||||
sourceUrl = page.fullurl,
|
||||
wikipediaUrl = wikipediaUrl,
|
||||
sourceName = context.getString(R.string.wikipedia_source),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import android.content.Context
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.data.Wikipedia
|
||||
import org.json.JSONObject
|
||||
|
||||
class WikipediaSerializer : SearchableSerializer {
|
||||
@ -14,9 +13,9 @@ class WikipediaSerializer : SearchableSerializer {
|
||||
json.put("label", searchable.label)
|
||||
json.put("text", searchable.text)
|
||||
json.put("id", searchable.id)
|
||||
json.put("image", searchable.image)
|
||||
json.put("image", searchable.imageUrl)
|
||||
json.put("wikipedia_url", searchable.wikipediaUrl)
|
||||
json.put("url", searchable.url)
|
||||
json.put("url", searchable.sourceUrl)
|
||||
return json.toString()
|
||||
}
|
||||
|
||||
@ -25,7 +24,7 @@ class WikipediaSerializer : SearchableSerializer {
|
||||
}
|
||||
|
||||
class WikipediaDeserializer(val context: Context) : SearchableDeserializer {
|
||||
override fun deserialize(serialized: String): SavableSearchable? {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
val json = JSONObject(serialized)
|
||||
val wikipediaUrl = json.optString("wikipedia_url").takeIf { !it.isNullOrBlank() } ?: return null
|
||||
val id = json.getLong("id")
|
||||
@ -33,9 +32,10 @@ class WikipediaDeserializer(val context: Context) : SearchableDeserializer {
|
||||
label = json.getString("label"),
|
||||
text = json.getString("text"),
|
||||
id = id,
|
||||
image = json.optString("image"),
|
||||
url = json.optString("url").takeIf { !it.isNullOrBlank() } ?: "${wikipediaUrl.padEnd(1, '/')}wiki?curid=$id",
|
||||
wikipediaUrl = wikipediaUrl
|
||||
imageUrl = json.optString("image"),
|
||||
sourceUrl = json.optString("url").takeIf { !it.isNullOrBlank() } ?: "${wikipediaUrl.padEnd(1, '/')}wiki?curid=$id",
|
||||
wikipediaUrl = wikipediaUrl,
|
||||
sourceName = context.getString(R.string.wikipedia_source),
|
||||
)
|
||||
}
|
||||
}
|
||||
310
docs/static/img/dependency-graph.dot
vendored
310
docs/static/img/dependency-graph.dot
vendored
@ -15,6 +15,7 @@ digraph {
|
||||
":core:ktx" [fillcolor="#94c1ff"];
|
||||
":core:permissions" [fillcolor="#94c1ff"];
|
||||
":core:preferences" [fillcolor="#94c1ff"];
|
||||
":core:shared" [fillcolor="#94c1ff"];
|
||||
":data:applications" [fillcolor="#fff694"];
|
||||
":data:appshortcuts" [fillcolor="#fff694"];
|
||||
":data:calculator" [fillcolor="#fff694"];
|
||||
@ -22,10 +23,11 @@ digraph {
|
||||
":data:contacts" [fillcolor="#fff694"];
|
||||
":data:currencies" [fillcolor="#fff694"];
|
||||
":data:customattrs" [fillcolor="#fff694"];
|
||||
":data:favorites" [fillcolor="#fff694"];
|
||||
":data:files" [fillcolor="#fff694"];
|
||||
":data:notifications" [fillcolor="#fff694"];
|
||||
":data:search-actions" [fillcolor="#fff694"];
|
||||
":data:searchable" [fillcolor="#fff694"];
|
||||
":data:themes" [fillcolor="#fff694"];
|
||||
":data:unitconverter" [fillcolor="#fff694"];
|
||||
":data:weather" [fillcolor="#fff694"];
|
||||
":data:websites" [fillcolor="#fff694"];
|
||||
@ -37,9 +39,11 @@ digraph {
|
||||
":libs:nextcloud" [fillcolor="#ad94ff"];
|
||||
":libs:owncloud" [fillcolor="#ad94ff"];
|
||||
":libs:webdav" [fillcolor="#ad94ff"];
|
||||
":plugins:sdk" [];
|
||||
":services:accounts" [fillcolor="#ff9498"];
|
||||
":services:backup" [fillcolor="#ff9498"];
|
||||
":services:badges" [fillcolor="#ff9498"];
|
||||
":services:favorites" [fillcolor="#ff9498"];
|
||||
":services:global-actions" [fillcolor="#ff9498"];
|
||||
":services:icons" [fillcolor="#ff9498"];
|
||||
":services:music" [fillcolor="#ff9498"];
|
||||
@ -64,13 +68,13 @@ digraph {
|
||||
":app:app" -> ":core:crashreporter" [style=dotted]
|
||||
":app:app" -> ":data:currencies" [style=dotted]
|
||||
":app:app" -> ":data:customattrs" [style=dotted]
|
||||
":app:app" -> ":data:favorites" [style=dotted]
|
||||
":app:app" -> ":data:searchable" [style=dotted]
|
||||
":app:app" -> ":data:themes" [style=dotted]
|
||||
":app:app" -> ":data:files" [style=dotted]
|
||||
":app:app" -> ":libs:g-services" [style=dotted]
|
||||
":app:app" -> ":core:i18n" [style=dotted]
|
||||
":app:app" -> ":services:icons" [style=dotted]
|
||||
":app:app" -> ":core:ktx" [style=dotted]
|
||||
":app:app" -> ":libs:ms-services" [style=dotted]
|
||||
":app:app" -> ":services:music" [style=dotted]
|
||||
":app:app" -> ":libs:nextcloud" [style=dotted]
|
||||
":app:app" -> ":data:notifications" [style=dotted]
|
||||
@ -89,6 +93,7 @@ digraph {
|
||||
":app:app" -> ":data:search-actions" [style=dotted]
|
||||
":app:app" -> ":services:global-actions" [style=dotted]
|
||||
":app:app" -> ":services:widgets" [style=dotted]
|
||||
":app:app" -> ":services:favorites" [style=dotted]
|
||||
":app:ui" -> ":app:ui"
|
||||
":app:ui" -> ":libs:material-color-utilities" [style=dotted]
|
||||
":app:ui" -> ":core:base" [style=dotted]
|
||||
@ -107,7 +112,8 @@ digraph {
|
||||
":app:ui" -> ":data:calculator" [style=dotted]
|
||||
":app:ui" -> ":data:files" [style=dotted]
|
||||
":app:ui" -> ":data:widgets" [style=dotted]
|
||||
":app:ui" -> ":data:favorites" [style=dotted]
|
||||
":app:ui" -> ":data:searchable" [style=dotted]
|
||||
":app:ui" -> ":data:themes" [style=dotted]
|
||||
":app:ui" -> ":data:wikipedia" [style=dotted]
|
||||
":app:ui" -> ":services:badges" [style=dotted]
|
||||
":app:ui" -> ":core:crashreporter" [style=dotted]
|
||||
@ -118,112 +124,50 @@ digraph {
|
||||
":app:ui" -> ":data:unitconverter" [style=dotted]
|
||||
":app:ui" -> ":libs:nextcloud" [style=dotted]
|
||||
":app:ui" -> ":libs:g-services" [style=dotted]
|
||||
":app:ui" -> ":libs:ms-services" [style=dotted]
|
||||
":app:ui" -> ":libs:owncloud" [style=dotted]
|
||||
":app:ui" -> ":services:accounts" [style=dotted]
|
||||
":app:ui" -> ":services:backup" [style=dotted]
|
||||
":app:ui" -> ":data:search-actions" [style=dotted]
|
||||
":app:ui" -> ":services:global-actions" [style=dotted]
|
||||
":app:ui" -> ":services:widgets" [style=dotted]
|
||||
":core:base" -> ":core:base"
|
||||
":core:base" -> ":core:ktx" [style=dotted]
|
||||
":core:base" -> ":core:i18n" [style=dotted]
|
||||
":core:compat" -> ":core:compat"
|
||||
":core:crashreporter" -> ":core:crashreporter"
|
||||
":core:crashreporter" -> ":core:base" [style=dotted]
|
||||
":app:ui" -> ":services:favorites" [style=dotted]
|
||||
":core:shared" -> ":core:shared"
|
||||
":core:database" -> ":core:database"
|
||||
":core:database" -> ":core:i18n" [style=dotted]
|
||||
":core:database" -> ":core:ktx" [style=dotted]
|
||||
":core:i18n" -> ":core:i18n"
|
||||
":core:ktx" -> ":core:ktx"
|
||||
":core:permissions" -> ":core:permissions"
|
||||
":core:permissions" -> ":core:ktx" [style=dotted]
|
||||
":core:permissions" -> ":core:base" [style=dotted]
|
||||
":core:permissions" -> ":core:crashreporter" [style=dotted]
|
||||
":core:database" -> ":core:preferences" [style=dotted]
|
||||
":core:preferences" -> ":core:preferences"
|
||||
":core:preferences" -> ":core:ktx" [style=dotted]
|
||||
":core:preferences" -> ":core:i18n" [style=dotted]
|
||||
":core:preferences" -> ":core:base" [style=dotted]
|
||||
":core:preferences" -> ":core:crashreporter" [style=dotted]
|
||||
":core:preferences" -> ":libs:material-color-utilities" [style=dotted]
|
||||
":data:applications" -> ":data:applications"
|
||||
":data:applications" -> ":core:base" [style=dotted]
|
||||
":data:applications" -> ":core:ktx" [style=dotted]
|
||||
":data:applications" -> ":core:compat" [style=dotted]
|
||||
":data:appshortcuts" -> ":data:appshortcuts"
|
||||
":data:appshortcuts" -> ":data:applications" [style=dotted]
|
||||
":data:appshortcuts" -> ":core:permissions" [style=dotted]
|
||||
":data:appshortcuts" -> ":core:base" [style=dotted]
|
||||
":data:appshortcuts" -> ":core:ktx" [style=dotted]
|
||||
":data:calculator" -> ":data:calculator"
|
||||
":data:calculator" -> ":core:base" [style=dotted]
|
||||
":core:permissions" -> ":core:permissions"
|
||||
":core:permissions" -> ":core:ktx" [style=dotted]
|
||||
":core:permissions" -> ":core:base" [style=dotted]
|
||||
":core:permissions" -> ":core:crashreporter" [style=dotted]
|
||||
":core:compat" -> ":core:compat"
|
||||
":core:crashreporter" -> ":core:crashreporter"
|
||||
":core:crashreporter" -> ":core:base" [style=dotted]
|
||||
":core:i18n" -> ":core:i18n"
|
||||
":core:ktx" -> ":core:ktx"
|
||||
":core:base" -> ":core:base"
|
||||
":core:base" -> ":core:ktx" [style=dotted]
|
||||
":core:base" -> ":core:i18n" [style=dotted]
|
||||
":core:base" -> ":libs:material-color-utilities" [style=dotted]
|
||||
":data:calendar" -> ":data:calendar"
|
||||
":data:calendar" -> ":core:ktx" [style=dotted]
|
||||
":data:calendar" -> ":core:base" [style=dotted]
|
||||
":data:calendar" -> ":core:permissions" [style=dotted]
|
||||
":data:calendar" -> ":libs:material-color-utilities" [style=dotted]
|
||||
":data:contacts" -> ":data:contacts"
|
||||
":data:contacts" -> ":core:ktx" [style=dotted]
|
||||
":data:contacts" -> ":core:base" [style=dotted]
|
||||
":data:contacts" -> ":core:permissions" [style=dotted]
|
||||
":data:currencies" -> ":data:currencies"
|
||||
":data:currencies" -> ":core:ktx" [style=dotted]
|
||||
":data:currencies" -> ":core:i18n" [style=dotted]
|
||||
":data:currencies" -> ":core:database" [style=dotted]
|
||||
":data:currencies" -> ":core:crashreporter" [style=dotted]
|
||||
":data:customattrs" -> ":data:customattrs"
|
||||
":data:customattrs" -> ":core:database" [style=dotted]
|
||||
":data:customattrs" -> ":core:base" [style=dotted]
|
||||
":data:customattrs" -> ":core:ktx" [style=dotted]
|
||||
":data:customattrs" -> ":core:crashreporter" [style=dotted]
|
||||
":data:customattrs" -> ":data:favorites" [style=dotted]
|
||||
":data:favorites" -> ":data:favorites"
|
||||
":data:favorites" -> ":core:base" [style=dotted]
|
||||
":data:favorites" -> ":data:calendar" [style=dotted]
|
||||
":data:favorites" -> ":core:database" [style=dotted]
|
||||
":data:favorites" -> ":core:preferences" [style=dotted]
|
||||
":data:favorites" -> ":data:applications" [style=dotted]
|
||||
":data:favorites" -> ":data:appshortcuts" [style=dotted]
|
||||
":data:favorites" -> ":data:contacts" [style=dotted]
|
||||
":data:favorites" -> ":core:ktx" [style=dotted]
|
||||
":data:favorites" -> ":data:files" [style=dotted]
|
||||
":data:favorites" -> ":data:websites" [style=dotted]
|
||||
":data:favorites" -> ":data:wikipedia" [style=dotted]
|
||||
":data:favorites" -> ":services:badges" [style=dotted]
|
||||
":data:favorites" -> ":core:crashreporter" [style=dotted]
|
||||
":data:files" -> ":data:files"
|
||||
":data:files" -> ":core:base" [style=dotted]
|
||||
":data:files" -> ":core:ktx" [style=dotted]
|
||||
":data:files" -> ":libs:ms-services" [style=dotted]
|
||||
":data:files" -> ":libs:g-services" [style=dotted]
|
||||
":data:files" -> ":libs:nextcloud" [style=dotted]
|
||||
":data:files" -> ":libs:owncloud" [style=dotted]
|
||||
":data:files" -> ":core:i18n" [style=dotted]
|
||||
":data:files" -> ":core:permissions" [style=dotted]
|
||||
":data:files" -> ":core:crashreporter" [style=dotted]
|
||||
":data:notifications" -> ":data:notifications"
|
||||
":data:notifications" -> ":core:permissions" [style=dotted]
|
||||
":data:search-actions" -> ":data:search-actions"
|
||||
":data:search-actions" -> ":core:base" [style=dotted]
|
||||
":data:search-actions" -> ":core:database" [style=dotted]
|
||||
":data:search-actions" -> ":core:ktx" [style=dotted]
|
||||
":data:search-actions" -> ":core:preferences" [style=dotted]
|
||||
":data:search-actions" -> ":core:crashreporter" [style=dotted]
|
||||
":data:unitconverter" -> ":data:unitconverter"
|
||||
":data:unitconverter" -> ":core:preferences" [style=dotted]
|
||||
":data:unitconverter" -> ":data:currencies" [style=dotted]
|
||||
":data:unitconverter" -> ":core:base" [style=dotted]
|
||||
":data:unitconverter" -> ":core:i18n" [style=dotted]
|
||||
":data:weather" -> ":data:weather"
|
||||
":data:weather" -> ":core:database" [style=dotted]
|
||||
":data:weather" -> ":core:ktx" [style=dotted]
|
||||
":data:weather" -> ":core:crashreporter" [style=dotted]
|
||||
":data:weather" -> ":core:preferences" [style=dotted]
|
||||
":data:weather" -> ":core:permissions" [style=dotted]
|
||||
":data:weather" -> ":core:i18n" [style=dotted]
|
||||
":data:websites" -> ":data:websites"
|
||||
":data:websites" -> ":core:base" [style=dotted]
|
||||
":data:websites" -> ":core:ktx" [style=dotted]
|
||||
":data:calculator" -> ":data:calculator"
|
||||
":data:calculator" -> ":core:base" [style=dotted]
|
||||
":data:appshortcuts" -> ":data:appshortcuts"
|
||||
":data:appshortcuts" -> ":data:applications" [style=dotted]
|
||||
":data:appshortcuts" -> ":core:permissions" [style=dotted]
|
||||
":data:appshortcuts" -> ":core:base" [style=dotted]
|
||||
":data:appshortcuts" -> ":core:ktx" [style=dotted]
|
||||
":data:appshortcuts" -> ":core:crashreporter" [style=dotted]
|
||||
":data:widgets" -> ":data:widgets"
|
||||
":data:widgets" -> ":data:weather" [style=dotted]
|
||||
":data:widgets" -> ":data:calendar" [style=dotted]
|
||||
@ -233,40 +177,79 @@ digraph {
|
||||
":data:widgets" -> ":core:preferences" [style=dotted]
|
||||
":data:widgets" -> ":core:database" [style=dotted]
|
||||
":data:widgets" -> ":core:crashreporter" [style=dotted]
|
||||
":data:searchable" -> ":data:searchable"
|
||||
":data:searchable" -> ":core:base" [style=dotted]
|
||||
":data:searchable" -> ":data:calendar" [style=dotted]
|
||||
":data:searchable" -> ":core:database" [style=dotted]
|
||||
":data:searchable" -> ":core:preferences" [style=dotted]
|
||||
":data:searchable" -> ":core:ktx" [style=dotted]
|
||||
":data:searchable" -> ":data:wikipedia" [style=dotted]
|
||||
":data:searchable" -> ":services:badges" [style=dotted]
|
||||
":data:searchable" -> ":core:crashreporter" [style=dotted]
|
||||
":data:unitconverter" -> ":data:unitconverter"
|
||||
":data:unitconverter" -> ":core:preferences" [style=dotted]
|
||||
":data:unitconverter" -> ":data:currencies" [style=dotted]
|
||||
":data:unitconverter" -> ":core:base" [style=dotted]
|
||||
":data:unitconverter" -> ":core:i18n" [style=dotted]
|
||||
":data:themes" -> ":data:themes"
|
||||
":data:themes" -> ":core:base" [style=dotted]
|
||||
":data:themes" -> ":core:database" [style=dotted]
|
||||
":data:themes" -> ":core:crashreporter" [style=dotted]
|
||||
":data:themes" -> ":libs:material-color-utilities" [style=dotted]
|
||||
":data:customattrs" -> ":data:customattrs"
|
||||
":data:customattrs" -> ":core:database" [style=dotted]
|
||||
":data:customattrs" -> ":core:base" [style=dotted]
|
||||
":data:customattrs" -> ":core:ktx" [style=dotted]
|
||||
":data:customattrs" -> ":core:crashreporter" [style=dotted]
|
||||
":data:customattrs" -> ":data:searchable" [style=dotted]
|
||||
":data:weather" -> ":data:weather"
|
||||
":data:weather" -> ":core:database" [style=dotted]
|
||||
":data:weather" -> ":core:ktx" [style=dotted]
|
||||
":data:weather" -> ":core:crashreporter" [style=dotted]
|
||||
":data:weather" -> ":core:preferences" [style=dotted]
|
||||
":data:weather" -> ":core:permissions" [style=dotted]
|
||||
":data:weather" -> ":core:i18n" [style=dotted]
|
||||
":data:files" -> ":data:files"
|
||||
":data:files" -> ":core:base" [style=dotted]
|
||||
":data:files" -> ":core:ktx" [style=dotted]
|
||||
":data:files" -> ":libs:g-services" [style=dotted]
|
||||
":data:files" -> ":libs:nextcloud" [style=dotted]
|
||||
":data:files" -> ":libs:owncloud" [style=dotted]
|
||||
":data:files" -> ":core:i18n" [style=dotted]
|
||||
":data:files" -> ":core:permissions" [style=dotted]
|
||||
":data:files" -> ":core:crashreporter" [style=dotted]
|
||||
":data:files" -> ":core:preferences" [style=dotted]
|
||||
":data:websites" -> ":data:websites"
|
||||
":data:websites" -> ":core:base" [style=dotted]
|
||||
":data:websites" -> ":core:ktx" [style=dotted]
|
||||
":data:search-actions" -> ":data:search-actions"
|
||||
":data:search-actions" -> ":core:base" [style=dotted]
|
||||
":data:search-actions" -> ":core:database" [style=dotted]
|
||||
":data:search-actions" -> ":core:ktx" [style=dotted]
|
||||
":data:search-actions" -> ":core:preferences" [style=dotted]
|
||||
":data:search-actions" -> ":core:crashreporter" [style=dotted]
|
||||
":data:wikipedia" -> ":data:wikipedia"
|
||||
":data:wikipedia" -> ":core:preferences" [style=dotted]
|
||||
":data:wikipedia" -> ":core:base" [style=dotted]
|
||||
":data:wikipedia" -> ":core:ktx" [style=dotted]
|
||||
":data:wikipedia" -> ":core:crashreporter" [style=dotted]
|
||||
":libs:g-services" -> ":libs:g-services"
|
||||
":libs:g-services" -> ":core:i18n" [style=dotted]
|
||||
":libs:g-services" -> ":core:crashreporter" [style=dotted]
|
||||
":libs:material-color-utilities" -> ":libs:material-color-utilities"
|
||||
":libs:ms-services" -> ":libs:ms-services"
|
||||
":libs:ms-services" -> ":core:crashreporter" [style=dotted]
|
||||
":libs:nextcloud" -> ":libs:webdav"
|
||||
":libs:nextcloud" -> ":libs:nextcloud"
|
||||
":libs:nextcloud" -> ":core:i18n" [style=dotted]
|
||||
":libs:owncloud" -> ":libs:webdav"
|
||||
":libs:owncloud" -> ":libs:owncloud"
|
||||
":libs:owncloud" -> ":core:crashreporter" [style=dotted]
|
||||
":libs:owncloud" -> ":core:ktx" [style=dotted]
|
||||
":libs:owncloud" -> ":core:i18n" [style=dotted]
|
||||
":libs:webdav" -> ":libs:webdav"
|
||||
":libs:webdav" -> ":core:crashreporter" [style=dotted]
|
||||
":libs:webdav" -> ":core:ktx" [style=dotted]
|
||||
":services:accounts" -> ":services:accounts"
|
||||
":services:accounts" -> ":libs:g-services" [style=dotted]
|
||||
":services:accounts" -> ":libs:ms-services" [style=dotted]
|
||||
":services:accounts" -> ":libs:owncloud" [style=dotted]
|
||||
":services:accounts" -> ":libs:nextcloud" [style=dotted]
|
||||
":services:backup" -> ":services:backup"
|
||||
":services:backup" -> ":data:favorites" [style=dotted]
|
||||
":services:backup" -> ":data:widgets" [style=dotted]
|
||||
":services:backup" -> ":data:search-actions" [style=dotted]
|
||||
":services:backup" -> ":core:preferences" [style=dotted]
|
||||
":services:backup" -> ":core:ktx" [style=dotted]
|
||||
":services:backup" -> ":data:customattrs" [style=dotted]
|
||||
":data:contacts" -> ":data:contacts"
|
||||
":data:contacts" -> ":core:ktx" [style=dotted]
|
||||
":data:contacts" -> ":core:base" [style=dotted]
|
||||
":data:contacts" -> ":core:permissions" [style=dotted]
|
||||
":data:notifications" -> ":data:notifications"
|
||||
":data:notifications" -> ":core:permissions" [style=dotted]
|
||||
":data:applications" -> ":data:applications"
|
||||
":data:applications" -> ":core:base" [style=dotted]
|
||||
":data:applications" -> ":core:ktx" [style=dotted]
|
||||
":data:applications" -> ":core:compat" [style=dotted]
|
||||
":data:currencies" -> ":data:currencies"
|
||||
":data:currencies" -> ":core:ktx" [style=dotted]
|
||||
":data:currencies" -> ":core:i18n" [style=dotted]
|
||||
":data:currencies" -> ":core:database" [style=dotted]
|
||||
":data:currencies" -> ":core:crashreporter" [style=dotted]
|
||||
":plugins:sdk" -> ":plugins:sdk"
|
||||
":plugins:sdk" -> ":core:shared" [style=dotted]
|
||||
":services:badges" -> ":services:badges"
|
||||
":services:badges" -> ":core:ktx" [style=dotted]
|
||||
":services:badges" -> ":data:applications" [style=dotted]
|
||||
@ -275,11 +258,36 @@ digraph {
|
||||
":services:badges" -> ":core:preferences" [style=dotted]
|
||||
":services:badges" -> ":core:base" [style=dotted]
|
||||
":services:badges" -> ":data:files" [style=dotted]
|
||||
":services:global-actions" -> ":services:global-actions"
|
||||
":services:global-actions" -> ":core:preferences" [style=dotted]
|
||||
":services:global-actions" -> ":core:base" [style=dotted]
|
||||
":services:global-actions" -> ":core:i18n" [style=dotted]
|
||||
":services:global-actions" -> ":core:permissions" [style=dotted]
|
||||
":services:favorites" -> ":services:favorites"
|
||||
":services:favorites" -> ":core:base" [style=dotted]
|
||||
":services:favorites" -> ":core:i18n" [style=dotted]
|
||||
":services:favorites" -> ":data:searchable" [style=dotted]
|
||||
":services:backup" -> ":services:backup"
|
||||
":services:backup" -> ":data:searchable" [style=dotted]
|
||||
":services:backup" -> ":data:widgets" [style=dotted]
|
||||
":services:backup" -> ":data:search-actions" [style=dotted]
|
||||
":services:backup" -> ":core:preferences" [style=dotted]
|
||||
":services:backup" -> ":core:ktx" [style=dotted]
|
||||
":services:backup" -> ":data:customattrs" [style=dotted]
|
||||
":services:backup" -> ":data:themes" [style=dotted]
|
||||
":services:search" -> ":services:search"
|
||||
":services:search" -> ":data:calculator" [style=dotted]
|
||||
":services:search" -> ":data:unitconverter" [style=dotted]
|
||||
":services:search" -> ":data:customattrs" [style=dotted]
|
||||
":services:search" -> ":data:search-actions" [style=dotted]
|
||||
":services:search" -> ":core:base" [style=dotted]
|
||||
":services:search" -> ":core:preferences" [style=dotted]
|
||||
":services:search" -> ":core:crashreporter" [style=dotted]
|
||||
":services:search" -> ":core:ktx" [style=dotted]
|
||||
":services:music" -> ":services:music"
|
||||
":services:music" -> ":core:ktx" [style=dotted]
|
||||
":services:music" -> ":core:preferences" [style=dotted]
|
||||
":services:music" -> ":data:notifications" [style=dotted]
|
||||
":services:music" -> ":core:crashreporter" [style=dotted]
|
||||
":services:accounts" -> ":services:accounts"
|
||||
":services:accounts" -> ":libs:g-services" [style=dotted]
|
||||
":services:accounts" -> ":libs:owncloud" [style=dotted]
|
||||
":services:accounts" -> ":libs:nextcloud" [style=dotted]
|
||||
":services:icons" -> ":data:customattrs"
|
||||
":services:icons" -> ":services:icons"
|
||||
":services:icons" -> ":core:database" [style=dotted]
|
||||
@ -288,37 +296,37 @@ digraph {
|
||||
":services:icons" -> ":core:base" [style=dotted]
|
||||
":services:icons" -> ":data:applications" [style=dotted]
|
||||
":services:icons" -> ":core:crashreporter" [style=dotted]
|
||||
":services:music" -> ":services:music"
|
||||
":services:music" -> ":core:ktx" [style=dotted]
|
||||
":services:music" -> ":core:preferences" [style=dotted]
|
||||
":services:music" -> ":data:notifications" [style=dotted]
|
||||
":services:music" -> ":core:crashreporter" [style=dotted]
|
||||
":services:search" -> ":services:search"
|
||||
":services:search" -> ":data:applications" [style=dotted]
|
||||
":services:search" -> ":data:appshortcuts" [style=dotted]
|
||||
":services:search" -> ":data:calculator" [style=dotted]
|
||||
":services:search" -> ":data:calendar" [style=dotted]
|
||||
":services:search" -> ":data:contacts" [style=dotted]
|
||||
":services:search" -> ":data:files" [style=dotted]
|
||||
":services:search" -> ":data:unitconverter" [style=dotted]
|
||||
":services:search" -> ":data:websites" [style=dotted]
|
||||
":services:search" -> ":data:wikipedia" [style=dotted]
|
||||
":services:search" -> ":data:customattrs" [style=dotted]
|
||||
":services:search" -> ":data:search-actions" [style=dotted]
|
||||
":services:search" -> ":core:base" [style=dotted]
|
||||
":services:search" -> ":core:database" [style=dotted]
|
||||
":services:search" -> ":core:preferences" [style=dotted]
|
||||
":services:search" -> ":core:crashreporter" [style=dotted]
|
||||
":services:search" -> ":core:ktx" [style=dotted]
|
||||
":services:widgets" -> ":services:widgets"
|
||||
":services:widgets" -> ":core:base" [style=dotted]
|
||||
":services:widgets" -> ":core:i18n" [style=dotted]
|
||||
":services:widgets" -> ":data:widgets" [style=dotted]
|
||||
":services:global-actions" -> ":services:global-actions"
|
||||
":services:global-actions" -> ":core:preferences" [style=dotted]
|
||||
":services:global-actions" -> ":core:base" [style=dotted]
|
||||
":services:global-actions" -> ":core:i18n" [style=dotted]
|
||||
":services:global-actions" -> ":core:permissions" [style=dotted]
|
||||
":services:tags" -> ":services:tags"
|
||||
":services:tags" -> ":core:preferences" [style=dotted]
|
||||
":services:tags" -> ":core:base" [style=dotted]
|
||||
":services:tags" -> ":core:ktx" [style=dotted]
|
||||
":services:tags" -> ":core:crashreporter" [style=dotted]
|
||||
":services:tags" -> ":data:customattrs" [style=dotted]
|
||||
":services:tags" -> ":data:favorites" [style=dotted]
|
||||
":services:widgets" -> ":services:widgets"
|
||||
":services:widgets" -> ":core:base" [style=dotted]
|
||||
":services:widgets" -> ":core:i18n" [style=dotted]
|
||||
":services:widgets" -> ":data:widgets" [style=dotted]
|
||||
":services:tags" -> ":data:searchable" [style=dotted]
|
||||
":libs:nextcloud" -> ":libs:webdav"
|
||||
":libs:nextcloud" -> ":libs:nextcloud"
|
||||
":libs:nextcloud" -> ":core:i18n" [style=dotted]
|
||||
":libs:webdav" -> ":libs:webdav"
|
||||
":libs:webdav" -> ":core:crashreporter" [style=dotted]
|
||||
":libs:webdav" -> ":core:ktx" [style=dotted]
|
||||
":libs:g-services" -> ":libs:g-services"
|
||||
":libs:g-services" -> ":core:i18n" [style=dotted]
|
||||
":libs:g-services" -> ":core:crashreporter" [style=dotted]
|
||||
":libs:material-color-utilities" -> ":libs:material-color-utilities"
|
||||
":libs:owncloud" -> ":libs:webdav"
|
||||
":libs:owncloud" -> ":libs:owncloud"
|
||||
":libs:owncloud" -> ":core:crashreporter" [style=dotted]
|
||||
":libs:owncloud" -> ":core:ktx" [style=dotted]
|
||||
":libs:owncloud" -> ":core:i18n" [style=dotted]
|
||||
":libs:ms-services" -> ":libs:ms-services"
|
||||
":libs:ms-services" -> ":core:crashreporter" [style=dotted]
|
||||
}
|
||||
|
||||
BIN
docs/static/img/dependency-graph.dot.png
vendored
BIN
docs/static/img/dependency-graph.dot.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 2.0 MiB |
@ -4,7 +4,7 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.export
|
||||
import de.mm20.launcher2.preferences.import
|
||||
@ -22,7 +22,7 @@ import java.util.zip.ZipOutputStream
|
||||
class BackupManager(
|
||||
private val context: Context,
|
||||
private val dataStore: LauncherDataStore,
|
||||
private val searchableRepository: SearchableRepository,
|
||||
private val searchableRepository: SavableSearchableRepository,
|
||||
private val widgetRepository: WidgetRepository,
|
||||
private val searchActionRepository: SearchActionRepository,
|
||||
private val customAttrsRepository: CustomAttributesRepository,
|
||||
|
||||
@ -4,10 +4,8 @@ import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.graphics.BadgeDrawable
|
||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||
import de.mm20.launcher2.search.data.LegacyShortcut
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import de.mm20.launcher2.search.data.UnavailableShortcut
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
@ -17,53 +15,37 @@ class AppShortcutBadgeProvider(
|
||||
private val context: Context
|
||||
) : BadgeProvider {
|
||||
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
|
||||
if (searchable is LauncherShortcut) {
|
||||
val componentName = searchable.launcherShortcut.activity
|
||||
if (componentName == null) {
|
||||
if (searchable is AppShortcut) {
|
||||
val componentName = searchable.componentName
|
||||
val packageName = searchable.packageName
|
||||
if (componentName != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val icon = try {
|
||||
context.packageManager.getActivityIcon(
|
||||
componentName
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return@withContext
|
||||
}
|
||||
val badge = Badge(icon = BadgeDrawable(context, icon))
|
||||
send(badge)
|
||||
}
|
||||
} else if (packageName != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val icon = try {
|
||||
context.packageManager.getApplicationIcon(
|
||||
packageName
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return@withContext
|
||||
}
|
||||
val badge = Badge(icon = BadgeDrawable(context, icon))
|
||||
send(badge)
|
||||
}
|
||||
} else {
|
||||
send(null)
|
||||
return@channelFlow
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
val icon = try {
|
||||
context.packageManager.getActivityIcon(
|
||||
componentName
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return@withContext
|
||||
}
|
||||
val badge = Badge(icon = BadgeDrawable(context, icon))
|
||||
send(badge)
|
||||
}
|
||||
} else if (searchable is LegacyShortcut) {
|
||||
val packageName = searchable.packageName
|
||||
if (packageName == null) {
|
||||
send(null)
|
||||
return@channelFlow
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
val icon = try {
|
||||
context.packageManager.getApplicationIcon(
|
||||
packageName
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return@withContext
|
||||
}
|
||||
val badge = Badge(icon = BadgeDrawable(context, icon))
|
||||
send(badge)
|
||||
}
|
||||
} else if (searchable is UnavailableShortcut) {
|
||||
val packageName = searchable.packageName
|
||||
withContext(Dispatchers.IO) {
|
||||
val icon = try {
|
||||
context.packageManager.getApplicationIcon(
|
||||
packageName
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
return@withContext
|
||||
}
|
||||
val badge = Badge(icon = BadgeDrawable(context, icon))
|
||||
send(badge)
|
||||
}
|
||||
} else {
|
||||
send(null)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package de.mm20.launcher2.badges.providers
|
||||
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
@ -3,7 +3,7 @@ package de.mm20.launcher2.badges.providers
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.notifications.NotificationRepository
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.Application
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@ -15,8 +15,8 @@ class NotificationBadgeProvider : BadgeProvider, KoinComponent {
|
||||
private val notificationRepository: NotificationRepository by inject()
|
||||
|
||||
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
|
||||
if (searchable is LauncherApp) {
|
||||
val packageName = searchable.`package`
|
||||
if (searchable is Application) {
|
||||
val packageName = searchable.componentName.packageName
|
||||
notificationRepository.notifications.map {
|
||||
it.filter { it.packageName == packageName }
|
||||
}.collectLatest {
|
||||
|
||||
@ -1,33 +1,18 @@
|
||||
package de.mm20.launcher2.badges.providers
|
||||
|
||||
import de.mm20.launcher2.applications.AppRepository
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.badges.R
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class SuspendedAppsBadgeProvider : BadgeProvider, KoinComponent {
|
||||
private val appRepository: AppRepository by inject()
|
||||
|
||||
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
|
||||
if (searchable is LauncherApp) {
|
||||
val packageName = searchable.`package`
|
||||
appRepository.getSuspendedPackages().collectLatest {
|
||||
if (it.contains(packageName)) {
|
||||
send(
|
||||
Badge(
|
||||
iconRes = R.drawable.ic_badge_suspended
|
||||
)
|
||||
)
|
||||
} else {
|
||||
send(null)
|
||||
}
|
||||
}
|
||||
if (searchable is Application && searchable.isSuspended) {
|
||||
send(Badge(iconRes = R.drawable.ic_badge_suspended))
|
||||
} else {
|
||||
send(null)
|
||||
}
|
||||
|
||||
@ -2,15 +2,16 @@ package de.mm20.launcher2.badges.providers
|
||||
|
||||
import de.mm20.launcher2.badges.Badge
|
||||
import de.mm20.launcher2.badges.R
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.data.LauncherShortcut
|
||||
import de.mm20.launcher2.search.AppProfile
|
||||
import de.mm20.launcher2.search.AppShortcut
|
||||
import de.mm20.launcher2.search.Application
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class WorkProfileBadgeProvider : BadgeProvider {
|
||||
override fun getBadge(searchable: Searchable): Flow<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(
|
||||
Badge(
|
||||
iconRes = R.drawable.ic_badge_workprofile
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
package de.mm20.launcher2.services.favorites
|
||||
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.searchable.SearchableRepository
|
||||
import de.mm20.launcher2.searchable.SavableSearchableRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class FavoritesService(
|
||||
val searchableRepository: SearchableRepository,
|
||||
val searchableRepository: SavableSearchableRepository,
|
||||
) {
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user