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,
|
accountsModule,
|
||||||
applicationsModule,
|
applicationsModule,
|
||||||
appShortcutsModule,
|
appShortcutsModule,
|
||||||
|
baseModule,
|
||||||
calculatorModule,
|
calculatorModule,
|
||||||
backupModule,
|
backupModule,
|
||||||
badgesModule,
|
badgesModule,
|
||||||
|
|||||||
@ -9,9 +9,9 @@
|
|||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/LauncherTheme"
|
|
||||||
android:stateNotNeeded="true"
|
|
||||||
android:resumeWhilePausing="true"
|
android:resumeWhilePausing="true"
|
||||||
|
android:stateNotNeeded="true"
|
||||||
|
android:theme="@style/LauncherTheme"
|
||||||
android:windowSoftInputMode="stateHidden|adjustResize">
|
android:windowSoftInputMode="stateHidden|adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@ -31,9 +31,9 @@
|
|||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/LauncherTheme"
|
|
||||||
android:stateNotNeeded="true"
|
|
||||||
android:resumeWhilePausing="true"
|
android:resumeWhilePausing="true"
|
||||||
|
android:stateNotNeeded="true"
|
||||||
|
android:theme="@style/LauncherTheme"
|
||||||
android:windowSoftInputMode="stateHidden|adjustResize">
|
android:windowSoftInputMode="stateHidden|adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.ASSIST" />
|
<action android:name="android.intent.action.ASSIST" />
|
||||||
@ -70,6 +70,10 @@
|
|||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="de.mm20.launcher2.ui.launcher.SharedLauncherActivity" />
|
android:value="de.mm20.launcher2.ui.launcher.SharedLauncherActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".launcher.sheets.BindAndConfigureAppWidgetActivity"
|
||||||
|
/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.sheets
|
package de.mm20.launcher2.ui.launcher.sheets
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
@ -16,6 +15,7 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
|
|||||||
val customizeSearchableSheetShown = mutableStateOf<SavableSearchable?>(null)
|
val customizeSearchableSheetShown = mutableStateOf<SavableSearchable?>(null)
|
||||||
val editFavoritesSheetShown = mutableStateOf(false)
|
val editFavoritesSheetShown = mutableStateOf(false)
|
||||||
val hiddenItemsSheetShown = mutableStateOf(false)
|
val hiddenItemsSheetShown = mutableStateOf(false)
|
||||||
|
val widgetPickerSheetShown = mutableStateOf(false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
|
registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
|
||||||
@ -28,6 +28,7 @@ class LauncherBottomSheetManager(registryOwner: SavedStateRegistryOwner) :
|
|||||||
|
|
||||||
editFavoritesSheetShown.value = state?.getBoolean(FAVORITES) ?: false
|
editFavoritesSheetShown.value = state?.getBoolean(FAVORITES) ?: false
|
||||||
hiddenItemsSheetShown.value = state?.getBoolean(HIDDEN) ?: 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 {
|
override fun saveState(): Bundle {
|
||||||
return bundleOf(
|
return bundleOf(
|
||||||
FAVORITES to editFavoritesSheetShown.value,
|
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
|
hiddenItemsSheetShown.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showWidgetPickerSheet() {
|
||||||
|
widgetPickerSheetShown.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissWidgetPickerSheet() {
|
||||||
|
widgetPickerSheetShown.value = false
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PROVIDER = "bottom_sheet_manager"
|
private const val PROVIDER = "bottom_sheet_manager"
|
||||||
private const val FAVORITES = "favorites"
|
private const val FAVORITES = "favorites"
|
||||||
private const val HIDDEN = "hidden"
|
private const val HIDDEN = "hidden"
|
||||||
|
private const val WIDGETS = "widgets"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,4 +13,9 @@ fun LauncherBottomSheets() {
|
|||||||
if (bottomSheetManager.editFavoritesSheetShown.value) {
|
if (bottomSheetManager.editFavoritesSheetShown.value) {
|
||||||
EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() })
|
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 androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.ktx.animateTo
|
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.clock.ClockWidget
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
|
import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
|
||||||
import de.mm20.launcher2.widgets.ExternalWidget
|
import de.mm20.launcher2.widgets.ExternalWidget
|
||||||
@ -69,6 +70,7 @@ fun WidgetColumn(
|
|||||||
|
|
||||||
val viewModel: WidgetsVM = viewModel()
|
val viewModel: WidgetsVM = viewModel()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val bottomSheetManager = LocalBottomSheetManager.current
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) }
|
val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) }
|
||||||
|
|
||||||
@ -100,7 +102,6 @@ fun WidgetColumn(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var showAddDialog by remember { mutableStateOf(false) }
|
|
||||||
Column {
|
Column {
|
||||||
val widgets by viewModel.widgets.observeAsState(emptyList())
|
val widgets by viewModel.widgets.observeAsState(emptyList())
|
||||||
val swapThresholds = remember(widgets) {
|
val swapThresholds = remember(widgets) {
|
||||||
@ -196,118 +197,10 @@ fun WidgetColumn(
|
|||||||
if (!editMode) {
|
if (!editMode) {
|
||||||
onEditModeChange(true)
|
onEditModeChange(true)
|
||||||
} else {
|
} else {
|
||||||
if (viewModel.getAvailableBuiltInWidgets().isEmpty()) {
|
bottomSheetManager.showWidgetPickerSheet()
|
||||||
pickWidgetLauncher.launch(
|
|
||||||
Intent(
|
|
||||||
context,
|
|
||||||
PickAppWidgetActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showAddDialog = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.appwidget.AppWidgetManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.liveData
|
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import de.mm20.launcher2.widgets.ExternalWidget
|
import de.mm20.launcher2.widgets.ExternalWidget
|
||||||
import de.mm20.launcher2.widgets.Widget
|
import de.mm20.launcher2.widgets.Widget
|
||||||
import de.mm20.launcher2.widgets.WidgetRepository
|
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 kotlinx.coroutines.flow.map
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|||||||
@ -45,6 +45,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.materialcomponents.core)
|
implementation(libs.materialcomponents.core)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(libs.androidx.palette)
|
implementation(libs.androidx.palette)
|
||||||
|
|
||||||
implementation(project(":core:ktx"))
|
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
|
package de.mm20.launcher2.widgets
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetProviderInfo
|
||||||
import android.content.Context
|
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.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.database.AppDatabase
|
import de.mm20.launcher2.database.AppDatabase
|
||||||
import de.mm20.launcher2.database.entities.WidgetEntity
|
import de.mm20.launcher2.database.entities.WidgetEntity
|
||||||
@ -15,6 +20,8 @@ import java.io.File
|
|||||||
interface WidgetRepository {
|
interface WidgetRepository {
|
||||||
fun getWidgets(): Flow<List<Widget>>
|
fun getWidgets(): Flow<List<Widget>>
|
||||||
fun getInternalWidgets(): List<Widget>
|
fun getInternalWidgets(): List<Widget>
|
||||||
|
|
||||||
|
suspend fun getAppWidgets(): List<AppWidgetProviderInfo>
|
||||||
fun saveWidgets(widgets: List<Widget>)
|
fun saveWidgets(widgets: List<Widget>)
|
||||||
fun addWidget(widget: Widget, position: Int)
|
fun addWidget(widget: Widget, position: Int)
|
||||||
fun removeWidget(widget: Widget)
|
fun removeWidget(widget: Widget)
|
||||||
@ -47,6 +54,18 @@ internal class WidgetRepositoryImpl(
|
|||||||
return listOf(WeatherWidget, MusicWidget, CalendarWidget, FavoritesWidget)
|
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>) {
|
override fun saveWidgets(widgets: List<Widget>) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user