New widget picker, add support for work profile widgets

This commit is contained in:
MM20 2023-04-07 17:22:38 +02:00
parent 057f0ff8fd
commit 309bee62fd
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
11 changed files with 612 additions and 121 deletions

View File

@ -57,6 +57,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
accountsModule,
applicationsModule,
appShortcutsModule,
baseModule,
calculatorModule,
backupModule,
badgesModule,

View File

@ -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>

View File

@ -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"
}
}

View File

@ -13,4 +13,9 @@ fun LauncherBottomSheets() {
if (bottomSheetManager.editFavoritesSheetShown.value) {
EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() })
}
if (bottomSheetManager.widgetPickerSheetShown.value) {
WidgetPickerSheet(
onDismiss = { bottomSheetManager.dismissWidgetPickerSheet() }
)
}
}

View File

@ -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
)
}
}
}
}
}
}
}
}
}
}

View File

@ -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>
)

View File

@ -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)
)
}
}
}
}
}
}
}

View File

@ -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

View File

@ -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"))

View 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 }
}

View File

@ -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 {