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