diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
index e978c718..ad741be5 100644
--- a/ui/src/main/AndroidManifest.xml
+++ b/ui/src/main/AndroidManifest.xml
@@ -47,6 +47,20 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="de.mm20.launcher2.ui.launcher.LauncherActivity" />
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsView.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsView.kt
index adce05e9..be01c4aa 100644
--- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsView.kt
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsView.kt
@@ -8,6 +8,7 @@ import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
+import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -28,6 +29,7 @@ import de.mm20.launcher2.transition.ChangingLayoutTransition
import de.mm20.launcher2.transition.OneShotLayoutTransition
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.databinding.ViewWidgetsBinding
+import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
import de.mm20.launcher2.ui.legacy.component.WidgetView
import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetType
@@ -49,7 +51,6 @@ class WidgetsView @JvmOverloads constructor(
private lateinit var widgets: MutableList
private val pickWidgetLauncher: ActivityResultLauncher
- private val configureWidgetLauncher: ActivityResultLauncher
init {
context as AppCompatActivity
@@ -57,34 +58,14 @@ class WidgetsView @JvmOverloads constructor(
layoutTransition = ChangingLayoutTransition()
binding.widgetList.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
- configureWidgetLauncher = context.registerForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) {
- val data = it.data ?: return@registerForActivityResult
- if (it.resultCode == Activity.RESULT_OK) {
- bindAppWidget(data)
- }
- }
-
pickWidgetLauncher = context.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
val data = it.data ?: return@registerForActivityResult
- val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
- if (widgetId == -1) return@registerForActivityResult
+ val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
+ if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@registerForActivityResult
if (it.resultCode == Activity.RESULT_OK) {
- val appWidget = AppWidgetManager.getInstance(context)
- .getAppWidgetInfo(widgetId) ?: return@registerForActivityResult
- if (appWidget.configure != null) {
- val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
- intent.component = appWidget.configure
- intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
- configureWidgetLauncher.launch(intent)
- } else {
- bindAppWidget(data)
- }
- } else {
- widgetHost.deleteAppWidgetId(widgetId)
+ bindAppWidget(widgetId)
}
}
@@ -231,18 +212,12 @@ class WidgetsView @JvmOverloads constructor(
}
@Suppress("DEPRECATION") // I don't care that neutral buttons are discouraged.
neutralButton(R.string.widget_add_external) {
- val appWidgetId = widgetHost.allocateAppWidgetId()
- val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
- pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
- pickWidgetLauncher.launch(pickIntent)
+ pickAppWidget()
it.dismiss()
}
}
} else {
- val appWidgetId = widgetHost.allocateAppWidgetId()
- val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
- pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
- pickWidgetLauncher.launch(pickIntent)
+ pickAppWidget()
}
}
@@ -253,9 +228,13 @@ class WidgetsView @JvmOverloads constructor(
widgetHost.deleteAppWidgetId(id)
}
- private fun bindAppWidget(data: Intent) {
- val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
- if (widgetId == -1) return
+ private fun pickAppWidget() {
+ val pickIntent = Intent(context, PickAppWidgetActivity::class.java)
+ pickWidgetLauncher.launch(pickIntent)
+ }
+
+ private fun bindAppWidget(widgetId: Int) {
+ if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return
val appWidget = AppWidgetManager.getInstance(context)
.getAppWidgetInfo(widgetId) ?: return
val widget = Widget(
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/AppWidgetList.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/AppWidgetList.kt
new file mode 100644
index 00000000..66aa16bc
--- /dev/null
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/AppWidgetList.kt
@@ -0,0 +1,110 @@
+package de.mm20.launcher2.ui.launcher.widgets.picker
+
+import android.appwidget.AppWidgetProviderInfo
+import android.graphics.drawable.Drawable
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import de.mm20.launcher2.ktx.isAtLeastApiLevel
+import de.mm20.launcher2.ui.component.LauncherCard
+import de.mm20.launcher2.ui.ktx.toDp
+
+@Composable
+fun AppWidgetList(
+ modifier: Modifier = Modifier,
+ widgets: List,
+ onWidgetSelected: (AppWidgetProviderInfo) -> Unit = {}
+) {
+ val context = LocalContext.current
+ val density = (LocalDensity.current.density * 160).toInt()
+ LazyColumn(
+ modifier = modifier
+ ) {
+ items(widgets) {
+ key(it.provider.toShortString()) {
+ LauncherCard(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ ) {
+ Column(
+ modifier = Modifier
+ .clickable {
+ onWidgetSelected(it)
+ }
+ .padding(16.dp),
+ ) {
+ val label = remember { it.loadLabel(context.packageManager) }
+ Text(text = label, style = MaterialTheme.typography.titleMedium)
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp)
+ ) {
+
+ val image: Drawable? = remember {
+ it.loadPreviewImage(context, density) ?: it.loadIcon(
+ context,
+ density
+ )
+ }
+
+ if (image != null) {
+
+ val mod =
+ if (image.intrinsicWidth > 0 && image.intrinsicHeight > 0) {
+ Modifier
+ .heightIn(max = image.intrinsicHeight.toDp())
+ .widthIn(max = image.intrinsicWidth.toDp())
+ .aspectRatio(
+ image.intrinsicWidth.toFloat() / image.intrinsicHeight.toFloat(),
+ matchHeightConstraintsFirst = true
+ )
+ } else {
+ Modifier.size(64.dp)
+ }
+
+ Canvas(
+ modifier = mod
+ ) {
+ drawIntoCanvas {
+ val aspectRatio =
+ image.intrinsicWidth / image.intrinsicHeight
+
+
+ image.setBounds(
+ 0,
+ 0,
+ size.width.toInt(),
+ size.height.toInt(),
+ )
+ image.draw(it.nativeCanvas)
+ }
+ }
+ }
+ }
+ if (isAtLeastApiLevel(31)) {
+ val description = remember { it.loadDescription(context)?.toString() }
+ if (description != null) {
+ Text(text = description, style = MaterialTheme.typography.bodySmall)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/PickAppWidgetActivity.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/PickAppWidgetActivity.kt
new file mode 100644
index 00000000..5fe4ec7d
--- /dev/null
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/PickAppWidgetActivity.kt
@@ -0,0 +1,170 @@
+package de.mm20.launcher2.ui.launcher.widgets.picker
+
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.compose.setContent
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import de.mm20.launcher2.preferences.LauncherDataStore
+import de.mm20.launcher2.preferences.Settings
+import de.mm20.launcher2.ui.MdcLauncherTheme
+import de.mm20.launcher2.ui.base.BaseActivity
+import de.mm20.launcher2.ui.component.ProvideIconShape
+import de.mm20.launcher2.ui.locals.LocalCardStyle
+import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import org.koin.android.ext.android.inject
+
+class PickAppWidgetActivity : BaseActivity() {
+
+ private val dataStore: LauncherDataStore by inject()
+ private val viewModel by viewModels()
+
+ private lateinit var widgetHost: AppWidgetHost
+ private lateinit var appWidgetManager: AppWidgetManager
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ widgetHost = AppWidgetHost(this, 44203)
+ appWidgetManager = AppWidgetManager.getInstance(this)
+
+ val availableWidgets = viewModel.getAvailableWidgets(this)
+ val selectedAppWidget = viewModel.selectedAppWidget
+ setContent {
+ MdcLauncherTheme {
+ val cardStyle by remember {
+ dataStore.data.map { it.cards }.distinctUntilChanged()
+ }.collectAsState(
+ Settings.CardSettings.getDefaultInstance()
+ )
+ val iconShape by remember {
+ dataStore.data.map {
+ if (it.easterEgg) Settings.IconSettings.IconShape.EasterEgg
+ else it.icons.shape
+ }.distinctUntilChanged()
+ }.collectAsState(Settings.IconSettings.IconShape.Circle)
+
+ val favoritesEnabled by remember {
+ dataStore.data.map { it.favorites.enabled }.distinctUntilChanged()
+ }.collectAsState(true)
+
+ CompositionLocalProvider(
+ LocalCardStyle provides cardStyle,
+ LocalFavoritesEnabled provides favoritesEnabled
+ ) {
+ ProvideIconShape(iconShape) {
+ val available by availableWidgets.observeAsState()
+ val selected by selectedAppWidget.observeAsState()
+ val widgets = available
+ if (selected == null) {
+ if (widgets != null) {
+ AppWidgetList(
+ modifier = Modifier.fillMaxSize(),
+ widgets = widgets,
+ onWidgetSelected = {
+ selectAppWidget(it)
+ }
+ )
+ } else {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun selectAppWidget(widget: AppWidgetProviderInfo) {
+ val appWidgetId = widgetHost.allocateAppWidgetId()
+ viewModel.selectAppWidget(widget, appWidgetId)
+ configureWidget()
+ }
+
+ private fun configureWidget() {
+ val appWidgetId = viewModel.appWidgetId.value ?: return
+ val widget = viewModel.selectedAppWidget.value ?: return
+ val canBind = appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, widget.provider)
+ Log.d("MM20", "Can bind: $canBind")
+ if (canBind) {
+ if (widget.configure != null) {
+ val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
+ intent.component = widget.configure
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ widgetHost.startAppWidgetConfigureActivityForResult(
+ this,
+ appWidgetId,
+ 0,
+ RequestCodeConfigure,
+ null
+ )
+ } else {
+ finishWithResult(appWidgetId)
+ }
+ } else {
+ startActivityForResult(
+ Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply {
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, widget.provider)
+ }, RequestCodeBind)
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ when(requestCode) {
+ RequestCodeBind -> {
+ if (resultCode == RESULT_OK) {
+ configureWidget()
+ } else {
+ viewModel.appWidgetId.value?.let { widgetHost.deleteAppWidgetId(it) }
+ cancel()
+ }
+ }
+ RequestCodeConfigure -> {
+ val widgetId = viewModel.appWidgetId.value ?: return cancel()
+ if (resultCode == RESULT_OK) {
+ finishWithResult(widgetId)
+ } else {
+ widgetHost.deleteAppWidgetId(widgetId)
+ cancel()
+ }
+ }
+ }
+ }
+
+ private fun finishWithResult(widgetId: Int) {
+ val data = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
+ setResult(RESULT_OK, data)
+ finish()
+ }
+
+ private fun cancel() {
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+
+ companion object {
+ const val RequestCodeConfigure = 1
+ const val RequestCodeBind = 2
+ }
+}
\ No newline at end of file
diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/PickAppWidgetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/PickAppWidgetVM.kt
new file mode 100644
index 00000000..0f02fcdc
--- /dev/null
+++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/PickAppWidgetVM.kt
@@ -0,0 +1,30 @@
+package de.mm20.launcher2.ui.launcher.widgets.picker
+
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.liveData
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class PickAppWidgetVM: ViewModel() {
+ var appWidgetId: MutableLiveData = MutableLiveData(null)
+ val selectedAppWidget: MutableLiveData = MutableLiveData(null)
+
+ fun selectAppWidget(appWidget: AppWidgetProviderInfo, appWidgetId: Int) {
+ this.appWidgetId.value = appWidgetId
+ this.selectedAppWidget.value = appWidget
+ }
+
+ fun getAvailableWidgets(context: Context): LiveData?> = liveData {
+ emit(null)
+ val appWidgetManager = AppWidgetManager.getInstance(context)
+ val widgets = withContext(Dispatchers.IO) {
+ appWidgetManager.installedProviders.sortedBy { it.loadLabel(context.packageManager) }
+ }
+ emit(widgets)
+ }
+}
\ No newline at end of file