Rewrite widget picker

Fix #12
This commit is contained in:
MM20 2022-02-16 23:08:06 +01:00
parent c66f574862
commit 3aa28671a4
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
5 changed files with 338 additions and 35 deletions

View File

@ -47,6 +47,20 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="de.mm20.launcher2.ui.launcher.LauncherActivity" />
</activity>
<activity
android:name=".launcher.widgets.picker.PickAppWidgetActivity"
android:exported="true"
android:label="@string/title_activity_settings"
android:launchMode="singleTask"
android:parentActivityName=".launcher.LauncherActivity"
android:screenOrientation="portrait"
android:taskAffinity="de.mm20.launcher2.settings"
android:theme="@style/SettingsTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="de.mm20.launcher2.ui.launcher.LauncherActivity" />
</activity>
</application>
</manifest>

View File

@ -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<Widget>
private val pickWidgetLauncher: ActivityResultLauncher<Intent>
private val configureWidgetLauncher: ActivityResultLauncher<Intent>
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(

View File

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

View File

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

View File

@ -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<Int?> = MutableLiveData(null)
val selectedAppWidget: MutableLiveData<AppWidgetProviderInfo?> = MutableLiveData(null)
fun selectAppWidget(appWidget: AppWidgetProviderInfo, appWidgetId: Int) {
this.appWidgetId.value = appWidgetId
this.selectedAppWidget.value = appWidget
}
fun getAvailableWidgets(context: Context): LiveData<List<AppWidgetProviderInfo>?> = liveData {
emit(null)
val appWidgetManager = AppWidgetManager.getInstance(context)
val widgets = withContext(Dispatchers.IO) {
appWidgetManager.installedProviders.sortedBy { it.loadLabel(context.packageManager) }
}
emit(widgets)
}
}