From 956af82f79995a5d6d3747f01e56f9f467f4dabb Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Tue, 29 Mar 2022 18:13:31 +0200 Subject: [PATCH] Refactor widgets --- .../main/res/drawable/anim_ic_edit_add.xml | 45 +- .../de/mm20/launcher2/database/WidgetDao.kt | 16 +- .../ui/launcher/LauncherScaffoldView.kt | 2 +- .../ui/launcher/widgets/WidgetItem.kt | 164 ++++++ .../ui/launcher/widgets/WidgetsVM.kt | 48 +- .../ui/launcher/widgets/WidgetsView.kt | 488 ++++++++++-------- .../widgets/external/ExternalWidget.kt | 58 +++ .../ui/legacy/component/WidgetView.kt | 120 ----- .../ui/legacy/view/WidgetResizeDragView.kt | 43 -- .../ui/legacy/widget/CalendarWidget.kt | 59 --- .../launcher2/ui/legacy/widget/ClockWidget.kt | 43 -- .../ui/legacy/widget/ExternalWidget.kt | 75 --- .../ui/legacy/widget/LauncherWidget.kt | 17 - .../launcher2/ui/legacy/widget/MusicWidget.kt | 47 -- .../ui/legacy/widget/WeatherWidget.kt | 50 -- .../res/layout/view_launcher_scaffold.xml | 3 +- ui/src/main/res/layout/view_widget.xml | 80 --- ui/src/main/res/layout/view_widgets.xml | 34 -- .../java/de/mm20/launcher2/widgets/Module.kt | 2 +- .../java/de/mm20/launcher2/widgets/Widget.kt | 183 ++++++- .../launcher2/widgets/WidgetRepository.kt | 73 ++- 21 files changed, 804 insertions(+), 846 deletions(-) create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WidgetView.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/view/WidgetResizeDragView.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ClockWidget.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ExternalWidget.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/LauncherWidget.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/WeatherWidget.kt delete mode 100644 ui/src/main/res/layout/view_widget.xml delete mode 100644 ui/src/main/res/layout/view_widgets.xml diff --git a/base/src/main/res/drawable/anim_ic_edit_add.xml b/base/src/main/res/drawable/anim_ic_edit_add.xml index 3329ff69..741278ab 100644 --- a/base/src/main/res/drawable/anim_ic_edit_add.xml +++ b/base/src/main/res/drawable/anim_ic_edit_add.xml @@ -8,36 +8,51 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - + + + + + + + + + diff --git a/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt b/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt index d1584204..1116806d 100644 --- a/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt @@ -1,7 +1,9 @@ package de.mm20.launcher2.database -import androidx.lifecycle.LiveData -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Transaction import de.mm20.launcher2.database.entities.WidgetEntity import kotlinx.coroutines.flow.Flow @@ -19,6 +21,16 @@ interface WidgetDao { @Insert fun insertAll(widgets: List) + @Insert + fun insert(widget: WidgetEntity) + @Query("DELETE FROM Widget") fun deleteAll() + + + @Query("DELETE FROM Widget WHERE data = :data AND type = :type") + fun deleteWidget(type: String, data: String) + + @Query("UPDATE Widget SET height = :newHeight WHERE data = :data AND type = :type") + fun updateHeight(type: String, data: String, newHeight: Int) } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldView.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldView.kt index f7439de5..143985b3 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldView.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldView.kt @@ -173,7 +173,7 @@ class LauncherScaffoldView @JvmOverloads constructor( } widgetsViewModel.isEditMode.observe(context) { - OneShotLayoutTransition.run(binding.scrollContainer) + //OneShotLayoutTransition.run(binding.scrollContainer) if (it) { binding.scrollView.setOnTouchListener(null) binding.searchBar.visibility = View.INVISIBLE diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt new file mode 100644 index 00000000..48cb35ba --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt @@ -0,0 +1,164 @@ +package de.mm20.launcher2.ui.launcher.widgets + +import android.app.Activity +import android.appwidget.AppWidgetHost +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.gestures.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.compose.ui.zIndex +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.LauncherCard +import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidget +import de.mm20.launcher2.ui.launcher.widgets.external.ExternalWidget +import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget +import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget +import de.mm20.launcher2.widgets.* +import java.lang.Integer.max +import kotlin.math.roundToInt + +@Composable +fun WidgetItem( + widget: Widget, + appWidgetHost: AppWidgetHost, + modifier: Modifier = Modifier, + editMode: Boolean = false, + onWidgetResize: (newHeight: Int) -> Unit = {}, + onWidgetRemove: () -> Unit = {}, + draggableState: DraggableState = rememberDraggableState {}, + onDragStopped: () -> Unit = {} +) { + val context = LocalContext.current + var resizeMode by remember(editMode) { mutableStateOf(false) } + + var isDragged by remember { mutableStateOf(false) } + val elevation by animateDpAsState(if (isDragged) 8.dp else 2.dp) + + LauncherCard( + modifier = modifier.zIndex(if (isDragged) 1f else 0f), + elevation = elevation + ) { + Column { + AnimatedVisibility(editMode) { + Row( + modifier = Modifier.padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Rounded.DragIndicator, + contentDescription = null, + modifier = Modifier.draggable( + state = draggableState, + orientation = Orientation.Vertical, + startDragImmediately = true, + onDragStarted = { + isDragged = true + }, + onDragStopped = { + isDragged = false + onDragStopped() + } + ) + ) + Text( + text = remember(widget) { widget.loadLabel(context) }, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + if (widget is ExternalWidget) { + IconButton(onClick = { resizeMode = !resizeMode }) { + Icon( + imageVector = Icons.Rounded.Edit, + contentDescription = stringResource(R.string.widget_action_adjust_height) + ) + } + } + if (widget.isConfigurable) { + IconButton({ + widget.configure(context as Activity, appWidgetHost) + }) { + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = stringResource(R.string.settings) + ) + } + } + IconButton(onClick = { onWidgetRemove() }) { + Icon( + imageVector = Icons.Rounded.Delete, + contentDescription = stringResource(R.string.widget_action_remove) + ) + } + } + } + AnimatedVisibility(!editMode || resizeMode) { + when (widget) { + is WeatherWidget -> { + WeatherWidget() + } + is MusicWidget -> { + MusicWidget() + } + is CalendarWidget -> { + CalendarWidget() + } + is ExternalWidget -> { + var height by remember(widget) { mutableStateOf(widget.height) } + Column { + ExternalWidget( + appWidgetHost = appWidgetHost, + widgetId = widget.widgetId, + modifier = Modifier.fillMaxWidth(), + height = height, + ) + if (resizeMode) { + val density = LocalDensity.current + val drgStt = rememberDraggableState { + height = max( + height + (it / density.density).roundToInt(), + widget.widgetProviderInfo.minResizeHeight + ) + } + Icon( + imageVector = Icons.Rounded.DragHandle, + contentDescription = null, + modifier = Modifier + .padding(top = 12.dp) + .requiredHeight(24.dp) + .fillMaxWidth() + .draggable( + state = drgStt, + orientation = Orientation.Vertical, + startDragImmediately = true, + onDragStopped = { + onWidgetResize(height) + } + ) + ) + } + + } + } + } + } + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt index 1f9db8df..59b1c4f2 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt @@ -1,11 +1,17 @@ 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.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 org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -20,11 +26,47 @@ class WidgetsVM : ViewModel(), KoinComponent { isEditMode.value = editMode } - fun saveWidgets(widgets: List) { + fun addWidget(widget: Widget) { + widgetRepository.addWidget(widget, widgets.value?.size ?: 0) + } + + fun addAppWidget(context: Context, widgetId: Int) { + if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return + val appWidget = AppWidgetManager.getInstance(context) + .getAppWidgetInfo(widgetId) ?: return + val widget = ExternalWidget( + widgetProviderInfo = appWidget, + height = appWidget.minHeight, + widgetId = widgetId, + ) + addWidget(widget) + } + + fun removeWidget(widget: Widget) { + widgetRepository.removeWidget(widget) + } + + fun setWidgetHeight(widget: Widget, newHeight: Int) { + widgetRepository.setWidgetHeight(widget, newHeight) + } + + fun getAvailableBuiltInWidgets(): List { + return widgetRepository.getInternalWidgets().filter { + widgets.value?.contains(it)?.not() ?: false + } + } + + fun moveUp(index: Int) { + val widgets = widgets.value?.toMutableList() ?: return + val widget = widgets.removeAt(index) + widgets.add(index - 1, widget) widgetRepository.saveWidgets(widgets) } - fun getInternalWidgets(): List { - return widgetRepository.getInternalWidgets() + fun moveDown(index: Int) { + val widgets = widgets.value?.toMutableList() ?: return + val widget = widgets.removeAt(index) + widgets.add(index + 1, widget) + widgetRepository.saveWidgets(widgets) } } \ 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 519da74c..046c3d58 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 @@ -1,7 +1,5 @@ package de.mm20.launcher2.ui.launcher.widgets -import android.animation.LayoutTransition -import android.annotation.SuppressLint import android.app.Activity import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager @@ -9,259 +7,317 @@ 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 -import android.widget.LinearLayout +import android.widget.FrameLayout import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.iterator -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.afollestad.materialdialogs.MaterialDialog -import com.afollestad.materialdialogs.list.listItems -import com.balsikandar.crashreporter.CrashReporter -import de.mm20.launcher2.ktx.dp -import de.mm20.launcher2.transition.ChangingLayoutTransition -import de.mm20.launcher2.transition.OneShotLayoutTransition +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.MutableLiveData +import de.mm20.launcher2.ui.ClockWidget +import de.mm20.launcher2.ui.MdcLauncherTheme import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.databinding.ViewWidgetsBinding +import de.mm20.launcher2.ui.base.ProvideSettings +import de.mm20.launcher2.ui.ktx.toDp 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 -import kotlinx.coroutines.awaitCancellation +import de.mm20.launcher2.widgets.ExternalWidget import kotlinx.coroutines.launch -import java.util.* -import kotlin.math.roundToInt +@OptIn(ExperimentalAnimationGraphicsApi::class) class WidgetsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null -) : LinearLayout(context, attrs) { - - private val binding = ViewWidgetsBinding.inflate(LayoutInflater.from(context), this) +) : FrameLayout(context, attrs) { private val widgetHost: AppWidgetHost = AppWidgetHost(context.applicationContext, 44203) private val viewModel: WidgetsVM by (context as AppCompatActivity).viewModels() - private lateinit var widgets: MutableList - private val pickWidgetLauncher: ActivityResultLauncher + private val clockWidgetHeight = MutableLiveData(0) + init { context as AppCompatActivity - pickWidgetLauncher = context.registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { val data = it.data ?: return@registerForActivityResult - val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + 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) { - bindAppWidget(widgetId) + viewModel.addAppWidget(context, widgetId) } } - viewModel.widgets.observe(context) { - if (it != null && !::widgets.isInitialized) { - widgets = it.toMutableList() - initWidgets() - } - } - viewModel.isEditMode.observe(context) { - if (it) { - OneShotLayoutTransition.run(binding.widgetList) - binding.clockWidget.visibility = View.GONE + val composeView = ComposeView(context) + composeView.setContent { + MdcLauncherTheme { + ProvideSettings { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(8.dp) + ) { + val editMode by viewModel.isEditMode.observeAsState(false) + val clockHeight by clockWidgetHeight.observeAsState(0) + val scope = rememberCoroutineScope() + var showAddDialog by remember { mutableStateOf(false) } - for (v in binding.widgetList.iterator()) { - if (v is WidgetView) { - v.editMode = true - v.onResizeModeChange = { - OneShotLayoutTransition.run(binding.widgetList) - OneShotLayoutTransition.run(this) + AnimatedVisibility(!editMode) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(clockHeight.toDp()), + contentAlignment = Alignment.BottomCenter + ) { + ClockWidget( + modifier = Modifier.fillMaxWidth() + ) + } + } + val widgets by viewModel.widgets.observeAsState(emptyList()) + Column { + val swapThresholds = remember(widgets) { + Array(widgets.size) { floatArrayOf(0f, 0f) } + } + for ((i, widget) in widgets.withIndex()) { + key(if (widget is ExternalWidget) widget.widgetId else widget) { + var dragOffsetAfterSwap = remember { null } + val offsetY = remember(widgets) { Animatable(dragOffsetAfterSwap ?: 0f) } + + LaunchedEffect(widgets) { + dragOffsetAfterSwap = null + } + + WidgetItem( + widget = widget, + appWidgetHost = widgetHost, + editMode = editMode, + onWidgetRemove = { + if (widget is ExternalWidget) { + widgetHost.deleteAppWidgetId(widget.widgetId) + } + viewModel.removeWidget(widget) + }, + onWidgetResize = { + viewModel.setWidgetHeight(widget, it) + }, + modifier = Modifier + .fillMaxWidth() + .onPlaced { + swapThresholds[i][0] = it.positionInParent().y + swapThresholds[i][1] = it.positionInParent().y + it.size.height + } + .padding(top = 8.dp) + .offset { + IntOffset(0, offsetY.value.toInt()) + }, + draggableState = rememberDraggableState { + scope.launch { + val newOffset = offsetY.value + it + offsetY.snapTo(newOffset) + if (i > 0 && newOffset < (swapThresholds[i - 1][0] - swapThresholds[i - 1][1])) { + if (dragOffsetAfterSwap == null) { + dragOffsetAfterSwap = swapThresholds[i - 1][1] - swapThresholds[i - 1][0] + newOffset + viewModel.moveUp(i) + } + } + if (i < widgets.lastIndex && newOffset > (swapThresholds[i + 1][1] - swapThresholds[i + 1][0])) { + if (dragOffsetAfterSwap == null) { + dragOffsetAfterSwap = swapThresholds[i + 1][0] - swapThresholds[i + 1][1] + newOffset + viewModel.moveDown(i) + } + } + } + }, + onDragStopped = { + scope.launch { + offsetY.animateTo(0f) + } + } + ) + } + } + } + + val icon = + AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add) + ExtendedFloatingActionButton( + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterHorizontally), + icon = { + Icon( + painter = rememberAnimatedVectorPainter( + animatedImageVector = icon, + atEnd = !editMode + ), contentDescription = null + ) + }, + text = { + Text( + stringResource( + if (editMode) R.string.widget_add_widget + else R.string.menu_edit_widgets + ) + ) + }, onClick = { + if (!editMode) { + viewModel.setEditMode(true) + } else { + if (viewModel.getAvailableBuiltInWidgets().isEmpty()) { + 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 = RoundedCornerShape(16.dp), + 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(context), + style = MaterialTheme.typography.bodyLarge + ) + } + } + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + .clickable { + pickWidgetLauncher.launch( + Intent( + context, + PickAppWidgetActivity::class.java + ) + ) + } + .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) + ) + } + } + } + } } } - } - binding.fabEditWidget.apply { - setIconResource(R.drawable.ic_add) - setText(R.string.widget_add_widget) - setOnClickListener { - addWidget() - } - } - } else { - if (::widgets.isInitialized) { - viewModel.saveWidgets(widgets) - OneShotLayoutTransition.run(binding.widgetList) - } - binding.clockWidget.visibility = View.VISIBLE - for (v in binding.widgetList.iterator()) { - if (v is WidgetView) { - v.editMode = false - } - } - binding.fabEditWidget.apply { - setIconResource(R.drawable.ic_edit) - setText(R.string.menu_edit_widgets) - setOnClickListener { - viewModel.setEditMode(true) - } + + } } } - - context.lifecycleScope.launch { - context.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - widgetHost.startListening() - try { - awaitCancellation() - } finally { - // TODO: find out why there is a NPE thrown sometimes - try { - widgetHost.stopListening() - } catch (e: NullPointerException) { - CrashReporter.logException(e) - } - } - } - } - - binding.fabEditWidget.setOnClickListener { - viewModel.setEditMode(true) - } + addView(composeView) } fun setClockWidgetHeight(height: Int) { - val params = binding.clockWidget.layoutParams - params.height = height - binding.clockWidget.layoutParams = params - } - - - private fun initWidgets() { - binding.widgetList.removeAllViews() - val params = LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - params.topMargin = (8 * dp).roundToInt() - for (w in widgets) { - val view = WidgetView(context) - view.layoutParams = params - if (view.setWidget(w, widgetHost)) { - binding.widgetList.addDragView(view, view.getDragHandle()) - view.onRemove = { - OneShotLayoutTransition.run(binding.widgetList) - binding.widgetList.removeDragView(view) - removeWidget(view.widget) - } - view.onResizeModeChange = { - OneShotLayoutTransition.run(binding.widgetList) - } - } - } - - binding.widgetList.setOnViewSwapListener { _, firstPosition, _, secondPosition -> - Collections.swap(widgets, firstPosition, secondPosition) - } - } - - - @SuppressLint("CheckResult") - private fun addWidget() { - val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data } - val internalWidgets = - viewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) } - if (internalWidgets.isNotEmpty()) { - MaterialDialog(context).show { - title(R.string.widget_add_widget) - listItems(items = internalWidgets.map { it.label }) { dialog, index, _ -> - val widget = internalWidgets[index] - val view = WidgetView(this@WidgetsView.context) - val params = LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - params.topMargin = (8 * dp).roundToInt() - view.layoutParams = params - if (view.setWidget(widget, widgetHost)) { - view.editMode = true - binding.widgetList.addDragView(view, view.getDragHandle()) - view.onRemove = { - OneShotLayoutTransition.run(binding.widgetList) - OneShotLayoutTransition.run(this@WidgetsView) - binding.widgetList.removeDragView(view) - removeWidget(view.widget) - } - view.onResizeModeChange = { - OneShotLayoutTransition.run(binding.widgetList) - OneShotLayoutTransition.run(this@WidgetsView) - } - widgets.add(widget) - } - dialog.dismiss() - } - @Suppress("DEPRECATION") // I don't care that neutral buttons are discouraged. - neutralButton(R.string.widget_add_external) { - pickAppWidget() - it.dismiss() - } - } - } else { - pickAppWidget() - } - } - - private fun removeWidget(widget: Widget?) { - widget ?: return - widgets.remove(widget) - val id = widget.data.toIntOrNull() ?: return - widgetHost.deleteAppWidgetId(id) - } - - 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( - type = WidgetType.THIRD_PARTY, - data = widgetId.toString(), - height = appWidget.minHeight - ) - val params = LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - params.topMargin = (8 * dp).roundToInt() - val view = WidgetView(context) - view.layoutParams = params - if (view.setWidget(widget, widgetHost)) { - view.editMode = true - - binding.widgetList.addDragView(view, view.getDragHandle()) - view.onRemove = { - OneShotLayoutTransition.run(binding.widgetList) - OneShotLayoutTransition.run(this) - binding.widgetList.removeDragView(view) - removeWidget(view.widget) - } - view.onResizeModeChange = { - OneShotLayoutTransition.run(binding.widgetList) - OneShotLayoutTransition.run(this) - } - widgets.add(widget) - } + clockWidgetHeight.value = height } } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt new file mode 100644 index 00000000..a48ce5c8 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/ExternalWidget.kt @@ -0,0 +1,58 @@ +package de.mm20.launcher2.ui.launcher.widgets.external + +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.ListView +import android.widget.ScrollView +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.doOnNextLayout +import androidx.core.view.iterator +import de.mm20.launcher2.ui.ktx.toPixels + +@Composable +fun ExternalWidget( + appWidgetHost: AppWidgetHost, + widgetId: Int, + height: Int, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val widgetInfo = remember(widgetId) { + AppWidgetManager.getInstance(context).getAppWidgetInfo(widgetId) + } + val viewHeightPx = height.dp.toPixels() + key(widgetId) { + AndroidView( + modifier = modifier.fillMaxWidth().height(height.dp), + factory = { + val view = appWidgetHost.createView(it.applicationContext, widgetId, widgetInfo) + enableNestedScroll(view) + return@AndroidView view + }, + update = { + it.updateAppWidgetSize(null, 0, 0, it.width, height) + } + ) + } +} + +private fun enableNestedScroll(view: View) { + if (view is ViewGroup) { + for (child in view.iterator()) { + enableNestedScroll(child) + } + } + if (view is ListView || view is ScrollView) view.isNestedScrollingEnabled = true +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WidgetView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WidgetView.kt deleted file mode 100644 index 6b80b3bd..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/component/WidgetView.kt +++ /dev/null @@ -1,120 +0,0 @@ -package de.mm20.launcher2.ui.legacy.component - -import android.appwidget.AppWidgetHost -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import androidx.appcompat.widget.TooltipCompat -import androidx.core.view.get -import de.mm20.launcher2.ktx.dp -import de.mm20.launcher2.transition.OneShotLayoutTransition -import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.databinding.ViewWidgetBinding -import de.mm20.launcher2.ui.legacy.view.LauncherCardView -import de.mm20.launcher2.ui.legacy.widget.* -import de.mm20.launcher2.widgets.Widget -import de.mm20.launcher2.widgets.WidgetType - -class WidgetView : LauncherCardView { - - var onRemove: (() -> Unit)? = null - - - var widget: Widget? = null - - var widgetView: LauncherWidget? = null - - var editMode = false - set(value) { - OneShotLayoutTransition.run(this) - if (value) { - binding.widgetControlPanel.visibility = View.VISIBLE - val widget = binding.widgetWrapper[2] - widget.visibility = View.GONE - binding.widgetName.visibility = View.VISIBLE - visibility = View.VISIBLE - } else { - resizeMode = false - binding.widgetControlPanel.visibility = View.GONE - val widget = binding.widgetWrapper[2] as LauncherWidget - widget.visibility = View.VISIBLE - binding.widgetName.visibility = View.GONE - } - field = value - } - - private var resizeMode = false - set(value) { - if (value == field) return - onResizeModeChange?.invoke(value) - OneShotLayoutTransition.run(this) - OneShotLayoutTransition.run(binding.widgetWrapper) - if (value) { - binding.widgetResizeDragHandle.visibility = View.VISIBLE - val widget = binding.widgetWrapper[2] - widget.visibility = View.VISIBLE - binding.widgetName.visibility = View.GONE - } else { - binding.widgetResizeDragHandle.visibility = View.GONE - if (editMode) { - val widget = binding.widgetWrapper[2] - widget.visibility = View.GONE - binding.widgetName.visibility = View.VISIBLE - } - - } - field = value - } - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) - - private val binding = ViewWidgetBinding.inflate(LayoutInflater.from(context), this) - - init { - binding.widgetActionResize.setOnClickListener { - resizeMode = !resizeMode - } - binding.widgetActionRemove.setOnClickListener { - onRemove?.invoke() - } - - TooltipCompat.setTooltipText(binding.widgetActionResize, context.getString(R.string.widget_action_adjust_height)) - TooltipCompat.setTooltipText(binding.widgetActionRemove, context.getString(R.string.widget_action_remove)) - } - - var onResizeModeChange: ((Boolean) -> Unit)? = null - - fun setWidget(widget: Widget, widgetHost: AppWidgetHost): Boolean { - if (widget.type == WidgetType.INTERNAL) { - widgetView = when (widget.data) { - CalendarWidget.ID -> CalendarWidget(context) - WeatherWidget.ID -> WeatherWidget(context) - MusicWidget.ID -> MusicWidget(context) - else -> return false - } - binding.widgetActionResize.visibility = View.GONE - binding.widgetResizeDragHandle.resizeView = widgetView - binding.widgetWrapper.addView(widgetView, 2) - binding.widgetName.text = widgetView?.name - } else { - widgetView = ExternalWidget(context, widget, widgetHost) - binding.widgetResizeDragHandle.resizeView = widgetView - binding.widgetResizeDragHandle.onResize = { - widget.height = (it / dp).toInt() - } - binding.widgetWrapper.addView(widgetView, 2) - binding.widgetName.text = widgetView?.name - binding.widgetActionResize.visibility = View.VISIBLE - } - this.widget = widget - return true - } - - fun getDragHandle(): View { - return binding.widgetDragHandle - } - -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/WidgetResizeDragView.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/WidgetResizeDragView.kt deleted file mode 100644 index 1476e3e3..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/view/WidgetResizeDragView.kt +++ /dev/null @@ -1,43 +0,0 @@ -package de.mm20.launcher2.ui.legacy.view - -import android.content.Context -import android.util.AttributeSet -import android.util.Log -import android.view.MotionEvent -import android.view.MotionEvent.INVALID_POINTER_ID -import android.view.View -import android.widget.ImageView - -class WidgetResizeDragView : ImageView { - - var resizeView: View? = null - - private var lastY = 0f - - var onResize: ((Int) -> Unit)? = null - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) - - override fun onTouchEvent(event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - parent.requestDisallowInterceptTouchEvent(true) - val y = event.y - lastY = y - } - MotionEvent.ACTION_MOVE -> { - val y = event.y - val dY = y - lastY - val view = resizeView ?: return false - val params = view.layoutParams - val newHeight = (view.height + dY).toInt() - params.height = newHeight - onResize?.invoke(newHeight) - view.layoutParams = params - } - } - return true - } -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt deleted file mode 100644 index 9ed2d494..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/CalendarWidget.kt +++ /dev/null @@ -1,59 +0,0 @@ -package de.mm20.launcher2.ui.legacy.widget - -import android.content.Context -import android.util.AttributeSet -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.LocalAbsoluteTonalElevation -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.unit.dp -import de.mm20.launcher2.ui.MdcLauncherTheme -import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.base.ProvideSettings -import de.mm20.launcher2.ui.launcher.widgets.calendar.CalendarWidget - -class CalendarWidget : LauncherWidget { - - override val canResize: Boolean - get() = false - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super( - context, - attrs, - defStyleRes - ) - - init { - val composeView = ComposeView(context) - composeView.setContent { - MdcLauncherTheme { - ProvideSettings { - // TODO: Temporary solution until parent widget card is rewritten in Compose - CompositionLocalProvider( - LocalContentColor provides MaterialTheme.colorScheme.onSurface, - LocalAbsoluteTonalElevation provides 1.dp - ) { - Column { - CalendarWidget() - } - } - } - } - } - addView(composeView) - - } - - - override val name: String - get() = resources.getString(R.string.widget_name_calendar) - - - companion object { - const val ID = "calendar" - } -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ClockWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ClockWidget.kt deleted file mode 100644 index c12e459a..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ClockWidget.kt +++ /dev/null @@ -1,43 +0,0 @@ -package de.mm20.launcher2.ui.legacy.widget - -import android.animation.LayoutTransition -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout -import androidx.compose.ui.platform.ComposeView -import de.mm20.launcher2.ui.ClockWidget -import de.mm20.launcher2.ui.MdcLauncherTheme - -class ClockWidget : FrameLayout { - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super( - context, - attrs, - defStyleRes - ) - - val view = ComposeView(context) - - init { - clipToPadding = false - clipChildren = false - layoutTransition = LayoutTransition() - - val composeView = ComposeView(context) - - addView(composeView) - - composeView.layoutParams = - LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - - - composeView.setContent { - MdcLauncherTheme { - ClockWidget() - } - } - } -} - diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ExternalWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ExternalWidget.kt deleted file mode 100644 index e0bb8b50..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/ExternalWidget.kt +++ /dev/null @@ -1,75 +0,0 @@ -package de.mm20.launcher2.ui.legacy.widget - -import android.annotation.SuppressLint -import android.appwidget.AppWidgetHost -import android.appwidget.AppWidgetHostView -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProviderInfo -import android.content.Context -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.widget.ListView -import android.widget.ScrollView -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.doOnLayout -import androidx.core.view.doOnNextLayout -import androidx.core.view.get -import androidx.core.view.iterator -import de.mm20.launcher2.ktx.dp -import de.mm20.launcher2.widgets.Widget - -@SuppressLint("ViewConstructor") -class ExternalWidget( - context: Context, - val widget: Widget, - host: AppWidgetHost -) : LauncherWidget(context) { - - val widgetInfo: AppWidgetProviderInfo? - - val widgetView: View - - init { - val id = widget.data.toInt() - widgetInfo = AppWidgetManager.getInstance(context.applicationContext).getAppWidgetInfo(id) - widgetView = host.createView(context.applicationContext, id, widgetInfo) - ?: View(context) - if (widgetView is AppWidgetHostView && widgetView.childCount > 0) { - enableNestedScroll(widgetView[0]) - } - val h = widget.height * dp - val params = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, h.toInt()) - val p = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - layoutParams = params - widgetView.layoutParams = p - addView(widgetView) - } - - override fun setLayoutParams(params: ViewGroup.LayoutParams?) { - super.setLayoutParams(params) - params ?: return - doOnNextLayout { - val width = if (params.width > 0) params.width else it.width - val height = if (params.height > 0) params.height else widgetInfo?.minHeight ?: it.height - if (widgetView is AppWidgetHostView) { - widgetView.updateAppWidgetSize(Bundle(), 0, 0, width, height) - } - } - } - - private fun enableNestedScroll(view: View) { - if (view is ViewGroup) { - for (child in view.iterator()) { - enableNestedScroll(child) - } - } - if (view is ListView || view is ScrollView) view.isNestedScrollingEnabled = true - } - - override val canResize: Boolean - get() = true - override val name: String - get() = widgetInfo?.loadLabel(context.packageManager) ?: "" - -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/LauncherWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/LauncherWidget.kt deleted file mode 100644 index 995c74f5..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/LauncherWidget.kt +++ /dev/null @@ -1,17 +0,0 @@ -package de.mm20.launcher2.ui.legacy.widget - -import android.animation.LayoutTransition -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout - -abstract class LauncherWidget : FrameLayout { - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) - - abstract val canResize: Boolean - abstract val name: String - -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt deleted file mode 100644 index 59c80bff..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/MusicWidget.kt +++ /dev/null @@ -1,47 +0,0 @@ -package de.mm20.launcher2.ui.legacy.widget - -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.platform.ComposeView -import de.mm20.launcher2.ui.MdcLauncherTheme -import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.launcher.widgets.music.MusicWidget - -class MusicWidget : LauncherWidget { - - override val canResize: Boolean - get() = false - override val name: String - get() = context.getString(R.string.widget_name_music) - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) - - - init { - val composeView = ComposeView(context) - composeView.id = FrameLayout.generateViewId() - composeView.setContent { - MdcLauncherTheme { - // TODO: Temporary solution until parent widget card is rewritten in Compose - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { - Column { - MusicWidget() - } - } - } - } - addView(composeView) - } - - companion object { - const val ID = "music" - } -} - diff --git a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/WeatherWidget.kt b/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/WeatherWidget.kt deleted file mode 100644 index 8416e2e5..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/legacy/widget/WeatherWidget.kt +++ /dev/null @@ -1,50 +0,0 @@ -package de.mm20.launcher2.ui.legacy.widget - -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.platform.ComposeView -import de.mm20.launcher2.ui.MdcLauncherTheme -import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget - -class WeatherWidget : LauncherWidget { - - - override val canResize: Boolean - get() = false - override val name: String - get() = resources.getString(R.string.widget_name_weather) - - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) - - - init { - val composeView = ComposeView(context) - composeView.id = FrameLayout.generateViewId() - composeView.setContent { - MdcLauncherTheme { - // TODO: Temporary solution until parent widget card is rewritten in Compose - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { - Column { - WeatherWidget() - } - } - } - } - addView(composeView) - } - - companion object { - const val ID = "weather" - } -} - - diff --git a/ui/src/main/res/layout/view_launcher_scaffold.xml b/ui/src/main/res/layout/view_launcher_scaffold.xml index c6d21e4c..021aa003 100644 --- a/ui/src/main/res/layout/view_launcher_scaffold.xml +++ b/ui/src/main/res/layout/view_launcher_scaffold.xml @@ -37,8 +37,7 @@ android:layout_height="wrap_content" android:clipChildren="false" android:clipToPadding="false" - android:orientation="vertical" - android:layout_margin="8dp" /> + android:orientation="vertical" /> diff --git a/ui/src/main/res/layout/view_widget.xml b/ui/src/main/res/layout/view_widget.xml deleted file mode 100644 index 17fc4d1f..00000000 --- a/ui/src/main/res/layout/view_widget.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ui/src/main/res/layout/view_widgets.xml b/ui/src/main/res/layout/view_widgets.xml deleted file mode 100644 index 8a7a783a..00000000 --- a/ui/src/main/res/layout/view_widgets.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt b/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt index d1759bc0..75abf6ac 100644 --- a/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt +++ b/widgets/src/main/java/de/mm20/launcher2/widgets/Module.kt @@ -5,5 +5,5 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val widgetsModule = module { - single { WidgetRepository(androidContext()) } + single { WidgetRepositoryImpl(androidContext(), get()) } } \ No newline at end of file diff --git a/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt b/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt index e2dfd28f..cea16880 100644 --- a/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt +++ b/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt @@ -1,32 +1,173 @@ package de.mm20.launcher2.widgets +import android.app.Activity +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Build import de.mm20.launcher2.database.entities.WidgetEntity +import de.mm20.launcher2.ktx.tryStartActivity -data class Widget( - val type: WidgetType, - var data: String, - var height: Int, - val label: String = "" -) { - constructor(entity: WidgetEntity) : this( - type = if (entity.type == "internal") WidgetType.INTERNAL else WidgetType. THIRD_PARTY, - data = entity.data, - height = entity.height, - label = entity.label - ) +sealed class Widget { + abstract fun loadLabel(context: Context): String + abstract fun toDatabaseEntity(position: Int = -1): WidgetEntity + open val isConfigurable: Boolean = false + open fun configure(context: Activity, appWidgetHost: AppWidgetHost) {} - fun toDatabaseEntity(position: Int): WidgetEntity { + companion object { + fun fromDatabaseEntity(context: Context, entity: WidgetEntity): Widget? { + if (entity.type == WidgetType.INTERNAL.value) { + return when (entity.data) { + "weather" -> WeatherWidget + "music" -> MusicWidget + "calendar" -> CalendarWidget + else -> null + } + } else { + val widgetId = entity.data.toIntOrNull() ?: return null + val widgetInfo = + AppWidgetManager.getInstance(context).getAppWidgetInfo(widgetId) ?: return null + return ExternalWidget( + height = entity.height, + widgetId = widgetId, + widgetProviderInfo = widgetInfo + ) + } + } + } +} + + +object WeatherWidget : Widget() { + override fun loadLabel(context: Context): String { + return context.getString(R.string.widget_name_weather) + } + + override fun toDatabaseEntity(position: Int): WidgetEntity { return WidgetEntity( - type = if (type == WidgetType.INTERNAL) "internal" else "3rdparty", - label = label, - position = position, - height = height, - data = data + type = WidgetType.INTERNAL.value, + data = "weather", + height = -1, + position = position + ) + } + + override val isConfigurable: Boolean = true + + override fun configure(context: Activity, appWidgetHost: AppWidgetHost) { + val intent = Intent() + intent.component = ComponentName( + context.getPackageName(), + "de.mm20.launcher2.ui.settings.SettingsActivity" + ) + intent.putExtra( + "de.mm20.launcher2.settings.ROUTE", + "settings/widgets/weather" + ) + context.tryStartActivity(intent) + } +} + +object MusicWidget : Widget() { + override fun loadLabel(context: Context): String { + return context.getString(R.string.widget_name_music) + } + + override fun toDatabaseEntity(position: Int): WidgetEntity { + return WidgetEntity( + type = WidgetType.INTERNAL.value, + data = "music", + height = -1, + position = position + ) + } + + override val isConfigurable: Boolean = true + + override fun configure(context: Activity, appWidgetHost: AppWidgetHost) { + val intent = Intent() + intent.component = ComponentName( + context.getPackageName(), + "de.mm20.launcher2.ui.settings.SettingsActivity" + ) + intent.putExtra( + "de.mm20.launcher2.settings.ROUTE", + "settings/widgets/music" + ) + context.tryStartActivity(intent) + } +} + + +object CalendarWidget : Widget() { + override fun loadLabel(context: Context): String { + return context.getString(R.string.widget_name_calendar) + } + + override fun toDatabaseEntity(position: Int): WidgetEntity { + return WidgetEntity( + type = WidgetType.INTERNAL.value, + data = "calendar", + height = -1, + position = position + ) + } + + override val isConfigurable: Boolean = true + + override fun configure(context: Activity, appWidgetHost: AppWidgetHost) { + val intent = Intent() + intent.component = ComponentName( + context.getPackageName(), + "de.mm20.launcher2.ui.settings.SettingsActivity" + ) + intent.putExtra( + "de.mm20.launcher2.settings.ROUTE", + "settings/widgets/calendar" + ) + context.tryStartActivity(intent) + } +} + +class ExternalWidget( + var height: Int, + val widgetId: Int, + val widgetProviderInfo: AppWidgetProviderInfo +) : Widget() { + override fun loadLabel(context: Context): String { + return widgetProviderInfo.loadLabel(context.packageManager) + } + + override fun toDatabaseEntity(position: Int): WidgetEntity { + return WidgetEntity( + type = WidgetType.THIRD_PARTY.value, + data = widgetId.toString(), + height = height, + position = position + ) + } + + override val isConfigurable: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + widgetProviderInfo.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0 + } else { + false + } + + override fun configure(context: Activity, appWidgetHost: AppWidgetHost) { + appWidgetHost.startAppWidgetConfigureActivityForResult( + context, + widgetId, + 0, + 0, + null ) } } -enum class WidgetType { - INTERNAL, - THIRD_PARTY +enum class WidgetType(val value: String) { + INTERNAL("internal"), + THIRD_PARTY("3rdparty") } \ No newline at end of file diff --git a/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt b/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt index 3d003361..fb30ab6e 100644 --- a/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt +++ b/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt @@ -1,40 +1,79 @@ package de.mm20.launcher2.widgets import android.content.Context -import de.mm20.launcher2.widgets.R import de.mm20.launcher2.database.AppDatabase import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import java.util.concurrent.Executors -class WidgetRepository( - val context: Context -) { +interface WidgetRepository { + fun getWidgets(): Flow> + fun getInternalWidgets(): List + fun saveWidgets(widgets: List) + fun addWidget(widget: Widget, position: Int) + fun removeWidget(widget: Widget) + fun setWidgetHeight(widget: Widget, newHeight: Int) +} + +internal class WidgetRepositoryImpl( + private val context: Context, + private val database: AppDatabase, +) : WidgetRepository { private val scope = CoroutineScope(Job() + Dispatchers.Default) - fun getWidgets(): Flow> { - return AppDatabase.getInstance(context).widgetDao() + override fun getWidgets(): Flow> { + return database.widgetDao() .getWidgets() - .map { it.map { Widget(it) } } + .map { it.mapNotNull { Widget.fromDatabaseEntity(context, it) } } } - fun getInternalWidgets(): List { - return listOf( - Widget(WidgetType.INTERNAL, "weather", -1, context.getString(R.string.widget_name_weather)), - Widget(WidgetType.INTERNAL, "music", -1, context.getString(R.string.widget_name_music)), - Widget(WidgetType.INTERNAL, "calendar", -1, context.getString(R.string.widget_name_calendar)), - ) + override fun getInternalWidgets(): List { + return listOf(WeatherWidget, MusicWidget, CalendarWidget) } - fun saveWidgets(widgets: List) { + override fun saveWidgets(widgets: List) { scope.launch { withContext(Dispatchers.IO) { - AppDatabase.getInstance(context).widgetDao().updateWidgets(widgets.mapIndexed { i, widget -> widget.toDatabaseEntity(i) }) + database.widgetDao() + .updateWidgets(widgets.mapIndexed { i, widget -> widget.toDatabaseEntity(i) }) } } } + + override fun addWidget(widget: Widget, position: Int) { + scope.launch { + withContext(Dispatchers.IO) { + database.widgetDao() + .insert(widget.toDatabaseEntity(position)) + } + } + } + + override fun removeWidget(widget: Widget) { + scope.launch { + withContext(Dispatchers.IO) { + val ent = widget.toDatabaseEntity() + database.widgetDao().deleteWidget( + ent.type, + ent.data + ) + } + } + } + + override fun setWidgetHeight(widget: Widget, newHeight: Int) { + scope.launch { + withContext(Dispatchers.IO) { + val ent = widget.toDatabaseEntity() + database.widgetDao().updateHeight( + ent.type, + ent.data, + newHeight + ) + } + } + } + } \ No newline at end of file