From 3aa28671a4f421f180df3af007adf77cd29fab27 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Wed, 16 Feb 2022 23:08:06 +0100 Subject: [PATCH] Rewrite widget picker Fix #12 --- ui/src/main/AndroidManifest.xml | 14 ++ .../ui/launcher/widgets/WidgetsView.kt | 49 ++--- .../launcher/widgets/picker/AppWidgetList.kt | 110 ++++++++++++ .../widgets/picker/PickAppWidgetActivity.kt | 170 ++++++++++++++++++ .../widgets/picker/PickAppWidgetVM.kt | 30 ++++ 5 files changed, 338 insertions(+), 35 deletions(-) create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/AppWidgetList.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/PickAppWidgetActivity.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/picker/PickAppWidgetVM.kt 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