diff --git a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt index b5281b98..b79d1eba 100644 --- a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt +++ b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt @@ -57,6 +57,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory { accountsModule, applicationsModule, appShortcutsModule, + baseModule, calculatorModule, backupModule, badgesModule, diff --git a/app/ui/src/main/AndroidManifest.xml b/app/ui/src/main/AndroidManifest.xml index 3aa9158b..58823eef 100644 --- a/app/ui/src/main/AndroidManifest.xml +++ b/app/ui/src/main/AndroidManifest.xml @@ -9,9 +9,9 @@ android:excludeFromRecents="true" android:exported="true" android:launchMode="singleTask" - android:theme="@style/LauncherTheme" - android:stateNotNeeded="true" android:resumeWhilePausing="true" + android:stateNotNeeded="true" + android:theme="@style/LauncherTheme" android:windowSoftInputMode="stateHidden|adjustResize"> @@ -31,9 +31,9 @@ android:excludeFromRecents="true" android:exported="true" android:launchMode="singleTask" - android:theme="@style/LauncherTheme" - android:stateNotNeeded="true" android:resumeWhilePausing="true" + android:stateNotNeeded="true" + android:theme="@style/LauncherTheme" android:windowSoftInputMode="stateHidden|adjustResize"> @@ -70,6 +70,10 @@ android:name="android.support.PARENT_ACTIVITY" android:value="de.mm20.launcher2.ui.launcher.SharedLauncherActivity" /> + + \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt index 68e7b906..564eaad2 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheetManager.kt @@ -1,7 +1,6 @@ package de.mm20.launcher2.ui.launcher.sheets import android.os.Bundle -import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.staticCompositionLocalOf import androidx.core.os.bundleOf @@ -16,6 +15,7 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) : val customizeSearchableSheetShown = mutableStateOf(null) val editFavoritesSheetShown = mutableStateOf(false) val hiddenItemsSheetShown = mutableStateOf(false) + val widgetPickerSheetShown = mutableStateOf(false) init { registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event -> @@ -28,6 +28,7 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) : editFavoritesSheetShown.value = state?.getBoolean(FAVORITES) ?: false hiddenItemsSheetShown.value = state?.getBoolean(HIDDEN) ?: false + widgetPickerSheetShown.value = state?.getBoolean(WIDGETS) ?: false } }) } @@ -35,7 +36,8 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) : override fun saveState(): Bundle { return bundleOf( FAVORITES to editFavoritesSheetShown.value, - HIDDEN to hiddenItemsSheetShown.value + HIDDEN to hiddenItemsSheetShown.value, + WIDGETS to widgetPickerSheetShown.value, ) } @@ -63,10 +65,19 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) : hiddenItemsSheetShown.value = false } + fun showWidgetPickerSheet() { + widgetPickerSheetShown.value = true + } + + fun dismissWidgetPickerSheet() { + widgetPickerSheetShown.value = false + } + companion object { private const val PROVIDER = "bottom_sheet_manager" private const val FAVORITES = "favorites" private const val HIDDEN = "hidden" + private const val WIDGETS = "widgets" } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt index b11117c8..eebfab76 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt @@ -13,4 +13,9 @@ fun LauncherBottomSheets() { if (bottomSheetManager.editFavoritesSheetShown.value) { EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() }) } + if (bottomSheetManager.widgetPickerSheetShown.value) { + WidgetPickerSheet( + onDismiss = { bottomSheetManager.dismissWidgetPickerSheet() } + ) + } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt new file mode 100644 index 00000000..0dd8c5e5 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt @@ -0,0 +1,420 @@ +package de.mm20.launcher2.ui.launcher.sheets + +import android.app.Activity +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Process +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContract +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Clear +import androidx.compose.material.icons.rounded.ExpandMore +import androidx.compose.material.icons.rounded.LightMode +import androidx.compose.material.icons.rounded.MusicNote +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.Today +import androidx.compose.material.icons.rounded.Widgets +import androidx.compose.material.icons.rounded.Work +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.SearchBar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.BottomSheetDialog +import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity +import de.mm20.launcher2.widgets.CalendarWidget +import de.mm20.launcher2.widgets.ExternalWidget +import de.mm20.launcher2.widgets.FavoritesWidget +import de.mm20.launcher2.widgets.MusicWidget +import de.mm20.launcher2.widgets.WeatherWidget +import de.mm20.launcher2.widgets.Widget +import kotlin.math.roundToInt + +class BindAndConfigureAppWidgetActivity : Activity() { + private lateinit var appWidgetHost: AppWidgetHost + private lateinit var appWidgetManager: AppWidgetManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + appWidgetHost = AppWidgetHost(this, 44203) + appWidgetManager = AppWidgetManager.getInstance(this) + + val appWidgetProviderInfo = intent.getParcelableExtra( + ExtraAppWidgetProviderInfo + ) + if (appWidgetProviderInfo == null) { + finish() + return + } + + val widgetId = appWidgetHost.allocateAppWidgetId() + + val canBind = + appWidgetManager.bindAppWidgetIdIfAllowed( + widgetId, + appWidgetProviderInfo.profile, + appWidgetProviderInfo.provider, + null + ) + + if (canBind) { + configureAppWidget(appWidgetProviderInfo, widgetId) + } else { + startActivityForResult( + Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply { + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + putExtra( + AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, + appWidgetProviderInfo.provider + ) + putExtra( + AppWidgetManager.EXTRA_APPWIDGET_PROVIDER_PROFILE, + appWidgetProviderInfo.profile + ) + }, PickAppWidgetActivity.RequestCodeBind + ) + } + } + + private fun configureAppWidget(widget: AppWidgetProviderInfo, appWidgetId: Int) { + if (widget.configure != null) { + appWidgetHost.startAppWidgetConfigureActivityForResult( + this, + appWidgetId, + 0, + PickAppWidgetActivity.RequestCodeConfigure, + null + ) + } else { + finishWithResult(appWidgetId) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + RequestCodeBind -> { + val appWidgetId = + data?.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return + if (resultCode == RESULT_OK) { + val widget = appWidgetManager.getAppWidgetInfo(appWidgetId) + configureAppWidget(widget, appWidgetId) + } else { + appWidgetHost.deleteAppWidgetId(appWidgetId) + cancel() + } + } + + RequestCodeConfigure -> { + val appWidgetId = + data?.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return cancel() + if (resultCode == RESULT_OK) { + finishWithResult(appWidgetId) + } else { + appWidgetHost.deleteAppWidgetId(appWidgetId) + cancel() + } + } + } + } + + private fun finishWithResult(widgetId: Int) { + val data = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + data.putExtra(ExtraAppWidgetProviderInfo, intent.getParcelableExtra(ExtraAppWidgetProviderInfo)) + setResult(RESULT_OK, data) + finish() + } + + private fun cancel() { + setResult(RESULT_CANCELED) + finish() + } + + companion object { + const val RequestCodeConfigure = 1 + const val RequestCodeBind = 2 + const val ExtraAppWidgetProviderInfo = "extra_app_widget_provider_info" + } +} + +private class BindAndConfigureAppWidgetContract( +) : ActivityResultContract() { + override fun createIntent(context: Context, input: AppWidgetProviderInfo): Intent { + return Intent(context, BindAndConfigureAppWidgetActivity::class.java).apply { + putExtra(BindAndConfigureAppWidgetActivity.ExtraAppWidgetProviderInfo, input) + } + } + + override fun parseResult(resultCode: Int, intent: Intent?): Widget? { + if (resultCode == Activity.RESULT_OK) { + val widgetId = intent?.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) + val widgetProviderInfo = intent?.extras?.getParcelable( + BindAndConfigureAppWidgetActivity.ExtraAppWidgetProviderInfo + ) + + if (widgetId != null && widgetProviderInfo != null) { + return ExternalWidget( + height = widgetProviderInfo.minHeight, + widgetId = widgetId, + widgetProviderInfo = widgetProviderInfo, + ) + } + } + return null + } + +} + +@Composable +fun WidgetPickerSheet( + onDismiss: () -> Unit +) { + val context = LocalContext.current + val density = LocalDensity.current + val viewModel: WidgetPickerSheetVM = viewModel(factory = WidgetPickerSheetVM.Factory) + + val bindAppWidgetStarter = + rememberLauncherForActivityResult(BindAndConfigureAppWidgetContract()) { + if (it != null) { + viewModel.pickWidget(it) + onDismiss() + } + } + + + val appWidgetGroups by viewModel.appWidgetGroups.collectAsState(emptyList()) + val expandAllGroups by viewModel.expandAllGroups.collectAsState(false) + + val colorSurface = MaterialTheme.colorScheme.surface + + val query by viewModel.searchQuery.collectAsState("") + + BottomSheetDialog( + onDismissRequest = onDismiss, + title = { + Text(stringResource(R.string.widget_add_widget)) + }) { + val builtIn by viewModel.builtInWidgets.collectAsState(emptyList()) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + ) { + stickyHeader { + SearchBar( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawRect( + brush = Brush.verticalGradient( + 0.5f to colorSurface, + 0.5f to colorSurface.copy(alpha = 0f), + ) + ) + } + .padding(bottom = 16.dp, start = 16.dp, end = 16.dp), + query = query, + onQueryChange = { viewModel.search(it) }, + onSearch = {}, + active = false, + onActiveChange = {}, + placeholder = { + Text(stringResource(R.string.search_bar_placeholder)) + }, + leadingIcon = { + Icon(Icons.Rounded.Search, null) + }, + trailingIcon = { + if (query.isNotEmpty()) { + IconButton(onClick = { viewModel.search("") }) { + Icon(Icons.Rounded.Clear, null) + } + } + } + ) { + } + } + items(builtIn) { + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp, start = 16.dp, end = 16.dp), + onClick = { + viewModel.pickWidget(it) + onDismiss() + }) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = when (it) { + is WeatherWidget -> Icons.Rounded.LightMode + is CalendarWidget -> Icons.Rounded.Today + is MusicWidget -> Icons.Rounded.MusicNote + is FavoritesWidget -> Icons.Rounded.Star + else -> Icons.Rounded.Widgets + }, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp) + ) + Text( + text = it.loadLabel(context), + style = MaterialTheme.typography.titleSmall + ) + } + } + } + for (group in appWidgetGroups) { + val expanded = viewModel.expandedGroup.value == group.packageName || expandAllGroups + item( + key = group.packageName, + ) { + val background by animateColorAsState( + if (expanded) MaterialTheme.colorScheme.secondaryContainer + else Color.Transparent, + label = "background" + ) + val textColor by animateColorAsState( + if (expanded) MaterialTheme.colorScheme.onSecondaryContainer + else MaterialTheme.colorScheme.onSurface, + label = "textColor" + ) + Row( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 4.dp) + .clip(MaterialTheme.shapes.small) + .background(background) + .clickable(enabled = !expandAllGroups) { + viewModel.toggleGroup(group.packageName) + } + .padding(horizontal = 16.dp, vertical = 12.dp) + .animateItemPlacement(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = group.appName, + color = textColor, + style = MaterialTheme.typography.titleMedium + ) + val rotate by animateFloatAsState( + if (expanded) 180f else 0f, label = "expandIcon" + ) + if (!expandAllGroups) { + Icon( + modifier = Modifier.rotate(rotate), + imageVector = Icons.Rounded.ExpandMore, + contentDescription = null + ) + } + } + } + if (expanded) { + items( + group.widgets, + key = { it } + ) { + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .animateItemPlacement(), + onClick = { + bindAppWidgetStarter.launch(it) + }) { + val previewImage = remember(it.provider) { + it.loadPreviewImage(context, (160f * density.density).roundToInt()) + } + val icon = remember(it.provider) { + it.loadIcon(context, (160f * density.density).roundToInt()) + } + Column { + if (previewImage != null) { + AsyncImage( + modifier = Modifier + .fillMaxWidth() + .height(it.minHeight.dp.coerceIn(60.dp, 200.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(16.dp), + model = previewImage, contentDescription = null + ) + } + + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + model = icon, + contentDescription = null, + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp) + ) + Text( + modifier = Modifier.weight(1f), + text = it.loadLabel(context.packageManager), + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (it.profile != Process.myUserHandle()) { + Icon( + modifier = Modifier + .padding(start = 16.dp) + .size(24.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.tertiaryContainer) + .padding(4.dp), + imageVector = Icons.Rounded.Work, + contentDescription = null, + tint = MaterialTheme.colorScheme.onTertiaryContainer + ) + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt new file mode 100644 index 00000000..33cac32f --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt @@ -0,0 +1,132 @@ +package de.mm20.launcher2.ui.launcher.sheets + +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.content.pm.PackageManager +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import de.mm20.launcher2.ktx.normalize +import de.mm20.launcher2.widgets.Widget +import de.mm20.launcher2.widgets.WidgetRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +class WidgetPickerSheetVM( + private val widgetRepository: WidgetRepository, + private val context: Context, +) : ViewModel() { + + private val packageManager = context.packageManager + + val searchQuery = MutableStateFlow("") + + private val enabledWidgets = widgetRepository.getWidgets() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(100), emptyList()) + + private val allBuiltInWidgets = enabledWidgets.map { w -> + widgetRepository.getInternalWidgets().filter { !w.contains(it) } + }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100)) + + val builtInWidgets = allBuiltInWidgets + .combine(searchQuery) { widgets, query -> + if (query.isBlank()) return@combine widgets + withContext(Dispatchers.IO) { + val normalizedQuery = query.normalize() + widgets.filter { + it.loadLabel(context).normalize().contains(normalizedQuery) + } + } + }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100)) + + private val allAppWidgets = flow { + val widgets = widgetRepository.getAppWidgets() + emit(widgets) + }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100)) + + private val filteredAppWidgets = allAppWidgets + .combine(searchQuery) { widgets, query -> + if (query.isBlank()) return@combine widgets + withContext(Dispatchers.IO) { + val normalizedQuery = query.normalize() + widgets.filter { + if (it.loadLabel(packageManager).normalize().contains(normalizedQuery)) { + return@filter true + } + val pkg = it.provider.packageName + val appInfo = try { + packageManager.getApplicationInfo(pkg, 0) + } catch (e: PackageManager.NameNotFoundException) { + return@filter false + } + appInfo.loadLabel(packageManager).toString().normalize() + .contains(normalizedQuery) + } + } + } + .shareIn(viewModelScope, SharingStarted.WhileSubscribed(100)) + + val expandAllGroups = filteredAppWidgets.map { + it.size < 10 + } + + val appWidgetGroups = filteredAppWidgets.map { widgets -> + withContext(Dispatchers.Default) { + widgets + .sortedBy { it.loadLabel(packageManager).normalize() } + .groupBy { + it.provider.packageName + } + .map { + val pkg = it.key + val appInfo = try { + packageManager.getApplicationInfo(pkg, 0) + } catch (e: PackageManager.NameNotFoundException) { + return@map AppWidgetGroup("", pkg, emptyList()) + } + AppWidgetGroup(appInfo.loadLabel(packageManager).toString(), pkg, it.value) + } + .sortedBy { it.appName.normalize() } + } + }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100)) + + val expandedGroup = mutableStateOf(null) + + fun pickWidget(widget: Widget) { + val position = enabledWidgets.value.size + widgetRepository.addWidget(widget, position) + } + + fun toggleGroup(group: String) { + expandedGroup.value = if (expandedGroup.value == group) null else group + } + + fun search(query: String) { + searchQuery.value = query + } + + companion object : KoinComponent { + val Factory = viewModelFactory { + initializer { + WidgetPickerSheetVM(get(), get()) + } + } + } +} + +data class AppWidgetGroup( + val appName: String, + val packageName: String, + val widgets: List +) \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt index e986ec10..6da9505e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt @@ -54,6 +54,7 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.ktx.animateTo +import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity import de.mm20.launcher2.widgets.ExternalWidget @@ -69,6 +70,7 @@ fun WidgetColumn( val viewModel: WidgetsVM = viewModel() val context = LocalContext.current + val bottomSheetManager = LocalBottomSheetManager.current val lifecycleOwner = LocalLifecycleOwner.current val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) } @@ -100,7 +102,6 @@ fun WidgetColumn( modifier = modifier ) { val scope = rememberCoroutineScope() - var showAddDialog by remember { mutableStateOf(false) } Column { val widgets by viewModel.widgets.observeAsState(emptyList()) val swapThresholds = remember(widgets) { @@ -196,118 +197,10 @@ fun WidgetColumn( if (!editMode) { onEditModeChange(true) } else { - if (viewModel.getAvailableBuiltInWidgets().isEmpty()) { - pickWidgetLauncher.launch( - Intent( - context, - PickAppWidgetActivity::class.java - ) - ) - } else { - showAddDialog = true - } + bottomSheetManager.showWidgetPickerSheet() } }) } - - if (showAddDialog) { - val availableBuiltInWidgets = - remember { viewModel.getAvailableBuiltInWidgets() } - Dialog(onDismissRequest = { showAddDialog = false }) { - Surface( - tonalElevation = 16.dp, - shadowElevation = 16.dp, - shape = MaterialTheme.shapes.extraLarge, - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), - ) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = stringResource(R.string.widget_add_widget), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding( - start = 24.dp, - end = 24.dp, - top = 24.dp, - bottom = 8.dp - ) - ) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding( - top = 16.dp - ) - ) { - items(availableBuiltInWidgets) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - viewModel.addWidget(it) - showAddDialog = false - } - .padding( - horizontal = 24.dp, - vertical = 16.dp - ) - ) { - Text( - text = it.loadLabel(LocalContext.current), - style = MaterialTheme.typography.bodyLarge - ) - } - } - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - .clickable { - pickWidgetLauncher.launch( - Intent( - context, - PickAppWidgetActivity::class.java - ) - ) - showAddDialog = false - } - .padding( - horizontal = 24.dp, - vertical = 16.dp - ), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Rounded.Add, - contentDescription = null, - modifier = Modifier.padding(end = 8.dp) - ) - Text( - text = stringResource(R.string.widget_add_external), - style = MaterialTheme.typography.titleMedium - ) - } - } - } - - TextButton( - onClick = { showAddDialog = false }, - modifier = Modifier - .align(Alignment.End) - .padding(bottom = 16.dp, end = 24.dp) - ) { - Text( - stringResource(android.R.string.cancel) - ) - } - } - } - } - } } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt index f9ae71b0..652537eb 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt @@ -2,17 +2,12 @@ package de.mm20.launcher2.ui.launcher.widgets import android.appwidget.AppWidgetManager import android.content.Context -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData -import androidx.lifecycle.liveData import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.widgets.ExternalWidget import de.mm20.launcher2.widgets.Widget import de.mm20.launcher2.widgets.WidgetRepository -import de.mm20.launcher2.widgets.WidgetType -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import org.koin.core.component.KoinComponent import org.koin.core.component.inject diff --git a/core/base/build.gradle.kts b/core/base/build.gradle.kts index 0a3c27b5..9ad39643 100644 --- a/core/base/build.gradle.kts +++ b/core/base/build.gradle.kts @@ -45,6 +45,8 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.materialcomponents.core) + implementation(libs.koin.android) + implementation(libs.androidx.palette) implementation(project(":core:ktx")) diff --git a/core/base/src/main/java/de/mm20/launcher2/Module.kt b/core/base/src/main/java/de/mm20/launcher2/Module.kt new file mode 100644 index 00000000..feafdd17 --- /dev/null +++ b/core/base/src/main/java/de/mm20/launcher2/Module.kt @@ -0,0 +1,9 @@ +package de.mm20.launcher2 + +import android.content.pm.PackageManager +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module + +val baseModule = module { + factory { androidContext().packageManager } +} \ No newline at end of file diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt index c7611a10..1ecb03aa 100644 --- a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt @@ -1,6 +1,11 @@ package de.mm20.launcher2.widgets +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo import android.content.Context +import android.content.pm.LauncherApps +import android.content.pm.PackageManager +import android.util.Log import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.entities.WidgetEntity @@ -15,6 +20,8 @@ import java.io.File interface WidgetRepository { fun getWidgets(): Flow> fun getInternalWidgets(): List + + suspend fun getAppWidgets(): List fun saveWidgets(widgets: List) fun addWidget(widget: Widget, position: Int) fun removeWidget(widget: Widget) @@ -47,6 +54,18 @@ internal class WidgetRepositoryImpl( return listOf(WeatherWidget, MusicWidget, CalendarWidget, FavoritesWidget) } + override suspend fun getAppWidgets(): List { + val appWidgetManager = AppWidgetManager.getInstance(context) + val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val profiles = launcherApps.profiles + val widgets = mutableListOf() + withContext(Dispatchers.IO) { + for (profile in profiles) { + widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile)) + } + } + return widgets + } override fun saveWidgets(widgets: List) { scope.launch {