New widget picker, add support for work profile widgets
This commit is contained in:
parent
057f0ff8fd
commit
309bee62fd
@ -57,6 +57,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
|
||||
accountsModule,
|
||||
applicationsModule,
|
||||
appShortcutsModule,
|
||||
baseModule,
|
||||
calculatorModule,
|
||||
backupModule,
|
||||
badgesModule,
|
||||
|
||||
@ -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">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@ -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">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ASSIST" />
|
||||
@ -70,6 +70,10 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="de.mm20.launcher2.ui.launcher.SharedLauncherActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".launcher.sheets.BindAndConfigureAppWidgetActivity"
|
||||
/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@ -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<SavableSearchable?>(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"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,4 +13,9 @@ fun LauncherBottomSheets() {
|
||||
if (bottomSheetManager.editFavoritesSheetShown.value) {
|
||||
EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() })
|
||||
}
|
||||
if (bottomSheetManager.widgetPickerSheetShown.value) {
|
||||
WidgetPickerSheet(
|
||||
onDismiss = { bottomSheetManager.dismissWidgetPickerSheet() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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<AppWidgetProviderInfo>(
|
||||
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<AppWidgetProviderInfo>(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<AppWidgetProviderInfo, Widget?>() {
|
||||
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<AppWidgetProviderInfo>(
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<String?>(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<AppWidgetProviderInfo>
|
||||
)
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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"))
|
||||
|
||||
9
core/base/src/main/java/de/mm20/launcher2/Module.kt
Normal file
9
core/base/src/main/java/de/mm20/launcher2/Module.kt
Normal file
@ -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<PackageManager> { androidContext().packageManager }
|
||||
}
|
||||
@ -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<List<Widget>>
|
||||
fun getInternalWidgets(): List<Widget>
|
||||
|
||||
suspend fun getAppWidgets(): List<AppWidgetProviderInfo>
|
||||
fun saveWidgets(widgets: List<Widget>)
|
||||
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<AppWidgetProviderInfo> {
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||
val profiles = launcherApps.profiles
|
||||
val widgets = mutableListOf<AppWidgetProviderInfo>()
|
||||
withContext(Dispatchers.IO) {
|
||||
for (profile in profiles) {
|
||||
widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile))
|
||||
}
|
||||
}
|
||||
return widgets
|
||||
}
|
||||
|
||||
override fun saveWidgets(widgets: List<Widget>) {
|
||||
scope.launch {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user