From 3cda75bc77d32cc1d8ce7037161a86616d73d8b4 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Mon, 10 Apr 2023 01:15:27 +0200 Subject: [PATCH] Refactor widgets --- app/app/build.gradle.kts | 1 + .../de/mm20/launcher2/LauncherApplication.kt | 2 + app/ui/build.gradle.kts | 1 + .../mm20/launcher2/ui/base/ProvideSettings.kt | 3 +- .../mm20/launcher2/ui/common/FavoritesVM.kt | 3 +- .../ui/launcher/sheets/WidgetPickerSheet.kt | 35 +- .../ui/launcher/sheets/WidgetPickerSheetVM.kt | 26 +- .../ui/launcher/widgets/WidgetColumn.kt | 43 +- .../ui/launcher/widgets/WidgetItem.kt | 23 +- .../ui/launcher/widgets/WidgetsVM.kt | 37 +- .../clock/parts/FavoritesPartProvider.kt | 3 +- .../widgets/favorites/FavoritesWidgetVM.kt | 6 +- .../widgets/WidgetSettingsScreenVM.kt | 12 +- build.gradle.kts | 4 + .../23.json | 493 ++++++++++++++++++ .../de/mm20/launcher2/database/AppDatabase.kt | 21 +- .../de/mm20/launcher2/database/WidgetDao.kt | 52 +- .../database/entities/WidgetEntity.kt | 17 +- .../database/migrations/Migration_22_23.kt | 44 ++ .../main/java/de/mm20/launcher2/ktx/UUID.kt | 12 + data/widgets/build.gradle.kts | 3 + .../de/mm20/launcher2/widgets/AppWidget.kt | 56 ++ .../mm20/launcher2/widgets/CalendarWidget.kt | 52 ++ .../mm20/launcher2/widgets/WeatherWIdget.kt | 55 ++ .../java/de/mm20/launcher2/widgets/Widget.kt | 190 +++---- .../launcher2/widgets/WidgetRepository.kt | 139 ++--- .../mm20/launcher2/backup/BackupComponent.kt | 2 +- .../de/mm20/launcher2/backup/BackupManager.kt | 2 +- services/widgets/.gitignore | 1 + services/widgets/build.gradle.kts | 46 ++ services/widgets/consumer-rules.pro | 0 services/widgets/proguard-rules.pro | 21 + services/widgets/src/main/AndroidManifest.xml | 4 + .../services/widgets/BuiltInWidgetInfo.kt | 7 + .../mm20/launcher2/services/widgets/Module.kt | 9 + .../services/widgets/WidgetsService.kt | 71 +++ settings.gradle.kts | 4 + 37 files changed, 1153 insertions(+), 347 deletions(-) create mode 100644 core/database/schemas/de.mm20.launcher2.database.AppDatabase/23.json create mode 100644 core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_22_23.kt create mode 100644 core/ktx/src/main/java/de/mm20/launcher2/ktx/UUID.kt create mode 100644 data/widgets/src/main/java/de/mm20/launcher2/widgets/AppWidget.kt create mode 100644 data/widgets/src/main/java/de/mm20/launcher2/widgets/CalendarWidget.kt create mode 100644 data/widgets/src/main/java/de/mm20/launcher2/widgets/WeatherWIdget.kt create mode 100644 services/widgets/.gitignore create mode 100644 services/widgets/build.gradle.kts create mode 100644 services/widgets/consumer-rules.pro create mode 100644 services/widgets/proguard-rules.pro create mode 100644 services/widgets/src/main/AndroidManifest.xml create mode 100644 services/widgets/src/main/java/de/mm20/launcher2/services/widgets/BuiltInWidgetInfo.kt create mode 100644 services/widgets/src/main/java/de/mm20/launcher2/services/widgets/Module.kt create mode 100644 services/widgets/src/main/java/de/mm20/launcher2/services/widgets/WidgetsService.kt diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 7f96cba5..0d5b21f7 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -156,6 +156,7 @@ dependencies { implementation(project(":core:database")) implementation(project(":data:search-actions")) implementation(project(":services:global-actions")) + implementation(project(":services:widgets")) // Uncomment this if you want annoying notifications in your debug builds yelling at you how terrible your code is //debugImplementation(libs.leakcanary) diff --git a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt index b79d1eba..f59d2b39 100644 --- a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt +++ b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt @@ -30,6 +30,7 @@ import de.mm20.launcher2.permissions.permissionsModule import de.mm20.launcher2.preferences.preferencesModule import de.mm20.launcher2.searchactions.searchActionsModule import de.mm20.launcher2.services.tags.servicesTagsModule +import de.mm20.launcher2.services.widgets.widgetsServiceModule import de.mm20.launcher2.weather.weatherModule import kotlinx.coroutines.* import org.koin.android.ext.koin.androidContext @@ -81,6 +82,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory { widgetsModule, wikipediaModule, servicesTagsModule, + widgetsServiceModule, ) ) } diff --git a/app/ui/build.gradle.kts b/app/ui/build.gradle.kts index 619dfbb3..57c1b311 100644 --- a/app/ui/build.gradle.kts +++ b/app/ui/build.gradle.kts @@ -150,4 +150,5 @@ dependencies { implementation(project(":services:backup")) implementation(project(":data:search-actions")) implementation(project(":services:global-actions")) + implementation(project(":services:widgets")) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt index af7b57d5..db9add58 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/base/ProvideSettings.kt @@ -8,6 +8,7 @@ import de.mm20.launcher2.ui.component.ProvideIconShape import de.mm20.launcher2.ui.locals.LocalCardStyle import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled import de.mm20.launcher2.ui.locals.LocalGridSettings +import de.mm20.launcher2.widgets.FavoritesWidget import de.mm20.launcher2.widgets.WidgetRepository import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -35,7 +36,7 @@ fun ProvideSettings( val favoritesEnabled by remember { combine( - widgetRepository.isFavoritesWidgetEnabled(), + widgetRepository.exists(FavoritesWidget.Type), dataStore.data.map { it.favorites.enabled }, dataStore.data.map { it.clockWidget.favoritesPart }, ) { a, b, c -> a || b || c }.distinctUntilChanged() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt index 9ffc9687..88c018cf 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt @@ -8,6 +8,7 @@ import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.data.Tag +import de.mm20.launcher2.widgets.CalendarWidget import de.mm20.launcher2.widgets.WidgetRepository import kotlinx.coroutines.flow.* import org.koin.core.component.KoinComponent @@ -36,7 +37,7 @@ abstract class FavoritesVM : ViewModel(), KoinComponent { open val favorites: Flow> = selectedTag.flatMapLatest { tag -> if (tag == null) { val columns = dataStore.data.map { it.grid.columnCount } - val excludeCalendar = widgetRepository.isCalendarWidgetEnabled() + val excludeCalendar = widgetRepository.exists(CalendarWidget.Type) val includeFrequentlyUsed = dataStore.data.map { it.favorites.frequentlyUsed } val frequentlyUsedRows = dataStore.data.map { it.favorites.frequentlyUsedRows } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt index 0dd8c5e5..562b1462 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt @@ -61,11 +61,13 @@ import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity import de.mm20.launcher2.widgets.CalendarWidget -import de.mm20.launcher2.widgets.ExternalWidget +import de.mm20.launcher2.widgets.AppWidget +import de.mm20.launcher2.widgets.AppWidgetConfig import de.mm20.launcher2.widgets.FavoritesWidget import de.mm20.launcher2.widgets.MusicWidget import de.mm20.launcher2.widgets.WeatherWidget import de.mm20.launcher2.widgets.Widget +import java.util.UUID import kotlin.math.roundToInt class BindAndConfigureAppWidgetActivity : Activity() { @@ -191,9 +193,12 @@ private class BindAndConfigureAppWidgetContract( ) if (widgetId != null && widgetProviderInfo != null) { - return ExternalWidget( - height = widgetProviderInfo.minHeight, - widgetId = widgetId, + return AppWidget( + id = UUID.randomUUID(), + config = AppWidgetConfig( + height = widgetProviderInfo.minHeight, + widgetId = widgetId, + ), widgetProviderInfo = widgetProviderInfo, ) } @@ -277,7 +282,15 @@ fun WidgetPickerSheet( .fillMaxWidth() .padding(bottom = 16.dp, start = 16.dp, end = 16.dp), onClick = { - viewModel.pickWidget(it) + val id = UUID.randomUUID() + val widget = when(it.type) { + WeatherWidget.Type -> WeatherWidget(id) + CalendarWidget.Type -> CalendarWidget(id) + MusicWidget.Type -> MusicWidget(id) + FavoritesWidget.Type -> FavoritesWidget(id) + else -> return@OutlinedCard + } + viewModel.pickWidget(widget) onDismiss() }) { Row( @@ -285,18 +298,18 @@ fun WidgetPickerSheet( verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = when (it) { - is WeatherWidget -> Icons.Rounded.LightMode - is CalendarWidget -> Icons.Rounded.Today - is MusicWidget -> Icons.Rounded.MusicNote - is FavoritesWidget -> Icons.Rounded.Star + imageVector = when (it.type) { + WeatherWidget.Type -> Icons.Rounded.LightMode + CalendarWidget.Type -> Icons.Rounded.Today + MusicWidget.Type -> Icons.Rounded.MusicNote + FavoritesWidget.Type -> Icons.Rounded.Star else -> Icons.Rounded.Widgets }, contentDescription = null, modifier = Modifier.padding(end = 16.dp) ) Text( - text = it.loadLabel(context), + text = it.label, style = MaterialTheme.typography.titleSmall ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt index 33cac32f..ee470292 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheetVM.kt @@ -1,14 +1,21 @@ package de.mm20.launcher2.ui.launcher.sheets +import WidgetsService import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.content.pm.PackageManager +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Star import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.vector.ImageVector import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import de.mm20.launcher2.ktx.normalize +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.widgets.FavoritesWidget import de.mm20.launcher2.widgets.Widget import de.mm20.launcher2.widgets.WidgetRepository import kotlinx.coroutines.Dispatchers @@ -24,7 +31,7 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.get class WidgetPickerSheetVM( - private val widgetRepository: WidgetRepository, + private val widgetsService: WidgetsService, private val context: Context, ) : ViewModel() { @@ -32,11 +39,11 @@ class WidgetPickerSheetVM( val searchQuery = MutableStateFlow("") - private val enabledWidgets = widgetRepository.getWidgets() + private val enabledWidgets = widgetsService.getWidgets() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(100), emptyList()) private val allBuiltInWidgets = enabledWidgets.map { w -> - widgetRepository.getInternalWidgets().filter { !w.contains(it) } + widgetsService.getBuiltInWidgets().filter { b -> !w.any { it::class == b::class } } }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100)) val builtInWidgets = allBuiltInWidgets @@ -45,13 +52,13 @@ class WidgetPickerSheetVM( withContext(Dispatchers.IO) { val normalizedQuery = query.normalize() widgets.filter { - it.loadLabel(context).normalize().contains(normalizedQuery) + it.label.normalize().contains(normalizedQuery) } } }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100)) private val allAppWidgets = flow { - val widgets = widgetRepository.getAppWidgets() + val widgets = widgetsService.getAppWidgetProviders() emit(widgets) }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100)) @@ -105,7 +112,7 @@ class WidgetPickerSheetVM( fun pickWidget(widget: Widget) { val position = enabledWidgets.value.size - widgetRepository.addWidget(widget, position) + widgetsService.addWidget(widget, position) } fun toggleGroup(group: String) { @@ -123,10 +130,17 @@ class WidgetPickerSheetVM( } } } + } data class AppWidgetGroup( val appName: String, val packageName: String, val widgets: List +) + +data class BuiltInWidgetInfo( + val type: String, + @StringRes val label: Int, + val icon: ImageVector ) \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt index 6da9505e..e323addd 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt @@ -3,32 +3,19 @@ package de.mm20.launcher2.ui.launcher.widgets import android.app.Activity import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager -import android.content.Intent import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedVisibility 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.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Add import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -37,7 +24,6 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onPlaced @@ -45,19 +31,15 @@ import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.ktx.animateTo import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager -import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget -import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity -import de.mm20.launcher2.widgets.ExternalWidget +import de.mm20.launcher2.widgets.AppWidget import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch @@ -85,19 +67,6 @@ fun WidgetColumn( } } - val pickWidgetLauncher = - rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { - val data = it.data ?: return@rememberLauncherForActivityResult - val widgetId = data.getIntExtra( - AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID - ) - if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@rememberLauncherForActivityResult - if (it.resultCode == Activity.RESULT_OK) { - viewModel.addAppWidget(context, widgetId) - } - } - Column( modifier = modifier ) { @@ -109,7 +78,7 @@ fun WidgetColumn( } val widgetsWithIndex = remember(widgets) { widgets.withIndex() } for ((i, widget) in widgetsWithIndex) { - key(if (widget is ExternalWidget) widget.widgetId else widget) { + key(widget.id) { var dragOffsetAfterSwap = remember { null } val offsetY = remember(widgets) { mutableStateOf(dragOffsetAfterSwap ?: 0f) } @@ -122,13 +91,13 @@ fun WidgetColumn( appWidgetHost = widgetHost, editMode = editMode, onWidgetRemove = { - if (widget is ExternalWidget) { - widgetHost.deleteAppWidgetId(widget.widgetId) + if (widget is AppWidget) { + widgetHost.deleteAppWidgetId(widget.config.widgetId) } viewModel.removeWidget(widget) }, - onWidgetResize = { - viewModel.setWidgetHeight(widget, it) + onWidgetUpdate = { + viewModel.updateWidget(it) }, modifier = Modifier .fillMaxWidth() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt index 359fc92c..34216725 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt @@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher.widgets import android.app.Activity import android.appwidget.AppWidgetHost +import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.gestures.* @@ -29,7 +30,6 @@ import de.mm20.launcher2.ui.launcher.widgets.favorites.FavoritesWidget 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 @@ -38,7 +38,7 @@ fun WidgetItem( appWidgetHost: AppWidgetHost, modifier: Modifier = Modifier, editMode: Boolean = false, - onWidgetResize: (newHeight: Int) -> Unit = {}, + onWidgetUpdate: (widget: Widget) -> Unit = {}, onWidgetRemove: () -> Unit = {}, draggableState: DraggableState = rememberDraggableState {}, onDragStopped: () -> Unit = {} @@ -84,7 +84,7 @@ fun WidgetItem( overflow = TextOverflow.Ellipsis, maxLines = 1 ) - if (widget is ExternalWidget) { + if (widget is AppWidget) { IconButton(onClick = { resizeMode = !resizeMode }) { Icon( imageVector = Icons.Rounded.Edit, @@ -124,19 +124,19 @@ fun WidgetItem( is FavoritesWidget -> { FavoritesWidget() } - is ExternalWidget -> { - var height by remember(widget) { mutableStateOf(widget.height) } + is AppWidget -> { + var dragDelta by remember { mutableStateOf(0) } Column { ExternalWidget( appWidgetHost = appWidgetHost, - widgetId = widget.widgetId, + widgetId = widget.config.widgetId, modifier = Modifier.fillMaxWidth(), - height = height, + height = widget.config.height + dragDelta, ) if (resizeMode) { val density = LocalDensity.current val drgStt = rememberDraggableState { - height += (it / density.density).roundToInt() + dragDelta += (it / density.density).roundToInt() } Icon( imageVector = Icons.Rounded.DragHandle, @@ -150,7 +150,12 @@ fun WidgetItem( orientation = Orientation.Vertical, startDragImmediately = true, onDragStopped = { - onWidgetResize(height) + onWidgetUpdate(widget.copy( + config = widget.config.copy( + height = widget.config.height + dragDelta + ) + )) + dragDelta = 0 } ) ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt index 652537eb..9713d487 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetsVM.kt @@ -1,11 +1,9 @@ package de.mm20.launcher2.ui.launcher.widgets -import android.appwidget.AppWidgetManager -import android.content.Context +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import de.mm20.launcher2.preferences.LauncherDataStore -import de.mm20.launcher2.widgets.ExternalWidget import de.mm20.launcher2.widgets.Widget import de.mm20.launcher2.widgets.WidgetRepository import kotlinx.coroutines.flow.map @@ -19,49 +17,28 @@ class WidgetsVM : ViewModel(), KoinComponent { val editButton = dataStore.data.map { it.widgets.editButton }.asLiveData() - val widgets = widgetRepository.getWidgets().asLiveData() + val widgets = widgetRepository.get().asLiveData() - 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) + widgetRepository.delete(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 updateWidget(widget: Widget) { + widgetRepository.update(widget) } fun moveUp(index: Int) { val widgets = widgets.value?.toMutableList() ?: return val widget = widgets.removeAt(index) widgets.add(index - 1, widget) - widgetRepository.saveWidgets(widgets) + widgetRepository.set(widgets) } fun moveDown(index: Int) { val widgets = widgets.value?.toMutableList() ?: return val widget = widgets.removeAt(index) widgets.add(index + 1, widget) - widgetRepository.saveWidgets(widgets) + widgetRepository.set(widgets) } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/FavoritesPartProvider.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/FavoritesPartProvider.kt index 8d4fbff6..4bd0ffa1 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/FavoritesPartProvider.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/parts/FavoritesPartProvider.kt @@ -16,6 +16,7 @@ import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid +import de.mm20.launcher2.widgets.CalendarWidget import de.mm20.launcher2.widgets.WidgetRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -41,7 +42,7 @@ class FavoritesPartProvider : PartProvider, KoinComponent { if (layout == ClockWidgetLayout.Horizontal) c - 2 else c } }.collectAsState(0) - val excludeCalendar by remember { widgetRepository.isCalendarWidgetEnabled() }.collectAsState( + val excludeCalendar by remember { widgetRepository.exists(CalendarWidget.Type) }.collectAsState( true ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidgetVM.kt index 42fa6707..c95a1c46 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidgetVM.kt @@ -1,5 +1,6 @@ package de.mm20.launcher2.ui.launcher.widgets.favorites +import WidgetsService import androidx.lifecycle.viewModelScope import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout import de.mm20.launcher2.ui.common.FavoritesVM @@ -9,13 +10,16 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch +import org.koin.core.component.inject class FavoritesWidgetVM : FavoritesVM() { + private val widgetsService: WidgetsService by inject() + override val tagsExpanded: Flow = dataStore.data.map { it.ui.widgetTagsMultiline } .shareIn(viewModelScope, SharingStarted.Lazily) - private val isTopWidget = widgetRepository.isFavoritesWidgetFirst() + private val isTopWidget = widgetsService.isFavoritesWidgetFirst() private val clockWidgetFavSlots = dataStore.data.combine(isTopWidget) { data, isTop -> if (!isTop || !data.clockWidget.favoritesPart) 0 else { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/widgets/WidgetSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/widgets/WidgetSettingsScreenVM.kt index 1c37a1a9..a9cec24c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/widgets/WidgetSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/widgets/WidgetSettingsScreenVM.kt @@ -4,6 +4,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import de.mm20.launcher2.preferences.LauncherDataStore +import de.mm20.launcher2.widgets.CalendarWidget +import de.mm20.launcher2.widgets.FavoritesWidget +import de.mm20.launcher2.widgets.MusicWidget +import de.mm20.launcher2.widgets.WeatherWidget import de.mm20.launcher2.widgets.WidgetRepository import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -16,10 +20,10 @@ class WidgetSettingsScreenVM : ViewModel(), KoinComponent { private val widgetRepository: WidgetRepository by inject() private val dataStore: LauncherDataStore by inject() - val calendarWidget = widgetRepository.isCalendarWidgetEnabled().asLiveData() - val musicWidget = widgetRepository.isMusicWidgetEnabled().asLiveData() - val weatherWidget = widgetRepository.isWeatherWidgetEnabled().asLiveData() - val favoritesWidget = widgetRepository.isFavoritesWidgetEnabled().asLiveData() + val calendarWidget = widgetRepository.exists(CalendarWidget.Type).asLiveData() + val musicWidget = widgetRepository.exists(MusicWidget.Type).asLiveData() + val weatherWidget = widgetRepository.exists(WeatherWidget.Type).asLiveData() + val favoritesWidget = widgetRepository.exists(FavoritesWidget.Type).asLiveData() val editButton = dataStore.data.map { it.widgets.editButton }.asLiveData() fun setEditButton(editButton: Boolean) { viewModelScope.launch { diff --git a/build.gradle.kts b/build.gradle.kts index 47d4a8d2..48481b37 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + kotlin("plugin.serialization") version libs.versions.kotlin apply false + id("org.jetbrains.kotlin.android") version libs.versions.kotlin apply false +} buildscript { repositories { diff --git a/core/database/schemas/de.mm20.launcher2.database.AppDatabase/23.json b/core/database/schemas/de.mm20.launcher2.database.AppDatabase/23.json new file mode 100644 index 00000000..b46f2ea2 --- /dev/null +++ b/core/database/schemas/de.mm20.launcher2.database.AppDatabase/23.json @@ -0,0 +1,493 @@ +{ + "formatVersion": 1, + "database": { + "version": 23, + "identityHash": "cb1e4fa1db90a1afddc085714570430d", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "precipitation", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "precipProbability", + "columnName": "rainProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snowProbability", + "columnName": "snowProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `weight` REAL NOT NULL DEFAULT 0.0, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedSearchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinPosition", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true, + "defaultValue": "0.0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Currency", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))", + "fields": [ + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lastUpdate", + "columnName": "lastUpdate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "symbol" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `packageName` TEXT, `activityName` TEXT, `drawable` TEXT, `extras` TEXT, `iconPack` TEXT NOT NULL, `name` TEXT, `themed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activityName", + "columnName": "activityName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "themed", + "columnName": "themed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, `themed` INTEGER NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "themed", + "columnName": "themed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `config` TEXT, `position` INTEGER NOT NULL, `id` BLOB NOT NULL, `parentId` BLOB, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "config", + "columnName": "config", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "parentId", + "columnName": "parentId", + "affinity": "BLOB", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomAttributes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `value` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SearchAction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `type` TEXT NOT NULL, `data` TEXT, `label` TEXT, `icon` INTEGER, `color` INTEGER, `customIcon` TEXT, `options` TEXT, PRIMARY KEY(`position`))", + "fields": [ + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "customIcon", + "columnName": "customIcon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "options", + "columnName": "options", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "position" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cb1e4fa1db90a1afddc085714570430d')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt b/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt index 7c30c9ff..7bb9ab6e 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt @@ -21,10 +21,13 @@ import de.mm20.launcher2.database.migrations.Migration_18_19 import de.mm20.launcher2.database.migrations.Migration_19_20 import de.mm20.launcher2.database.migrations.Migration_20_21 import de.mm20.launcher2.database.migrations.Migration_21_22 +import de.mm20.launcher2.database.migrations.Migration_22_23 import de.mm20.launcher2.database.migrations.Migration_6_7 import de.mm20.launcher2.database.migrations.Migration_7_8 import de.mm20.launcher2.database.migrations.Migration_8_9 import de.mm20.launcher2.database.migrations.Migration_9_10 +import de.mm20.launcher2.ktx.toBytes +import java.util.UUID @Database( entities = [ @@ -36,7 +39,7 @@ import de.mm20.launcher2.database.migrations.Migration_9_10 WidgetEntity::class, CustomAttributeEntity::class, SearchActionEntity::class - ], version = 22, exportSchema = true + ], version = 23, exportSchema = true ) @TypeConverters(ComponentNameConverter::class, StringListConverter::class) abstract class AppDatabase : RoomDatabase() { @@ -81,10 +84,15 @@ abstract class AppDatabase : RoomDatabase() { ) db.execSQL( - "INSERT INTO Widget (type, data, height, position, label) VALUES " + - "('internal', 'weather', -1, 0, '${context.getString(R.string.widget_name_weather)}')," + - "('internal', 'music', -1, 1, '${context.getString(R.string.widget_name_music)}')," + - "('internal', 'calendar', -1, 2, '${context.getString(R.string.widget_name_calendar)}');" + "INSERT INTO Widget (`type`, `position`, `id`) VALUES " + + "('weather', 0, ?)," + + "('music', 1, ?)," + + "('calendar', 2, ?);", + arrayOf( + UUID.randomUUID().toBytes(), + UUID.randomUUID().toBytes(), + UUID.randomUUID().toBytes() + ) ) } }) @@ -104,7 +112,8 @@ abstract class AppDatabase : RoomDatabase() { Migration_18_19(), Migration_19_20(), Migration_20_21(), - Migration_21_22() + Migration_21_22(), + Migration_22_23(), ).build() if (_instance == null) _instance = instance return instance diff --git a/core/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt b/core/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt index 0d377c45..429b98b9 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt @@ -1,42 +1,54 @@ package de.mm20.launcher2.database import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.Query -import androidx.room.Transaction +import androidx.room.Update import de.mm20.launcher2.database.entities.WidgetEntity +import de.mm20.launcher2.database.entities.PartialWidgetEntity import kotlinx.coroutines.flow.Flow +import java.util.UUID @Dao interface WidgetDao { - @Query("SELECT * FROM Widget ORDER BY position ASC") - fun getWidgets(): Flow> + @Query("SELECT * FROM Widget WHERE parentId IS NULL ORDER BY position ASC LIMIT :limit OFFSET :offset") + fun queryRoot(limit: Int, offset: Int): Flow> - @Transaction - fun updateWidgets(widgets: List) { - deleteAll() - insertAll(widgets) - } + @Query("SELECT * FROM Widget WHERE parentId = :parentId ORDER BY position ASC LIMIT :limit OFFSET :offset") + fun queryByParent(parentId: UUID,limit: Int, offset: Int): Flow> @Insert - fun insertAll(widgets: List) + suspend fun insert(widget: WidgetEntity) @Insert - fun insert(widget: WidgetEntity) + suspend fun insert(widgets: List) - @Query("DELETE FROM Widget") - fun deleteAll() + @Update(entity = WidgetEntity::class) + suspend fun patch(widget: PartialWidgetEntity) + @Update(entity = WidgetEntity::class) + suspend fun patch(widgets: List) - @Query("DELETE FROM Widget WHERE data = :data AND type = :type") - fun deleteWidget(type: String, data: String) + @Update + suspend fun update(widget: WidgetEntity) - @Query("UPDATE Widget SET height = :newHeight WHERE data = :data AND type = :type") - fun updateHeight(type: String, data: String, newHeight: Int) + @Update + suspend fun update(widgets: List) - @Query("SELECT EXISTS(SELECT 1 FROM Widget WHERE type = :type AND data = :data)") - fun exists(type: String, data: String) : Flow + @Query("DELETE FROM Widget WHERE id = :id") + suspend fun delete(id: UUID) + + @Query("DELETE FROM WIDGET WHERE id IN (:ids)") + suspend fun delete(ids: List) + + @Query("DELETE FROM Widget WHERE parentId = :parentId") + suspend fun deleteByParent(parentId: UUID) + + @Query("DELETE FROM Widget WHERE parentId IS NULL") + suspend fun deleteRoot() + + @Query("SELECT EXISTS(SELECT 1 FROM Widget WHERE type = :type)") + fun exists(type: String): Flow - @Query("SELECT * FROM Widget ORDER BY position ASC LIMIT 1") - fun getFirst() : Flow } \ No newline at end of file diff --git a/core/database/src/main/java/de/mm20/launcher2/database/entities/WidgetEntity.kt b/core/database/src/main/java/de/mm20/launcher2/database/entities/WidgetEntity.kt index 97bbe8cf..00b4ab8a 100644 --- a/core/database/src/main/java/de/mm20/launcher2/database/entities/WidgetEntity.kt +++ b/core/database/src/main/java/de/mm20/launcher2/database/entities/WidgetEntity.kt @@ -2,14 +2,23 @@ package de.mm20.launcher2.database.entities import androidx.room.Entity import androidx.room.PrimaryKey +import java.util.UUID @Entity(tableName = "Widget") data class WidgetEntity( val type: String, - var data: String, - var height: Int, + var config: String?, var position: Int, - val label: String = "", - @PrimaryKey(autoGenerate = true) val id: Int? = null + @PrimaryKey val id: UUID, + val parentId: UUID? = null, +) + +/** + * Partial entity for updating and deleting + */ +data class PartialWidgetEntity( + val type: String, + var config: String?, + @PrimaryKey val id: UUID, ) \ No newline at end of file diff --git a/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_22_23.kt b/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_22_23.kt new file mode 100644 index 00000000..1bae4402 --- /dev/null +++ b/core/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_22_23.kt @@ -0,0 +1,44 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import de.mm20.launcher2.ktx.toBytes +import org.koin.core.component.KoinComponent +import java.util.UUID + +class Migration_22_23 : Migration(22, 23), KoinComponent { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Widget RENAME TO Widget_old") + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `Widget` ( + `type` TEXT NOT NULL, + `config` TEXT, + `position` INTEGER NOT NULL, + `id` BLOB NOT NULL, + `parentId` BLOB, + PRIMARY KEY(`id`) + ) + """ + ) + val oldWidgets = + database.query("SELECT `type`, `data`, `height`, `position` FROM `Widget_old`") + while (oldWidgets.moveToNext()) { + val oldType = oldWidgets.getString(0) + val data = oldWidgets.getString(1) + val newType = if (oldType == "3rdparty") "app" else data + val height = oldWidgets.getInt(2) + val position = oldWidgets.getInt(3) + val id = UUID.randomUUID() + val config = if (oldType == "3rdparty") { + "{\"widgetId\": $data, \"height\": $height}" + } else null + database.execSQL( + "INSERT INTO `Widget` (`type`, `config`, `position`, `id`) VALUES (?, ?, ?, ?)", + arrayOf(newType, config, position, id.toBytes()) + ) + } + oldWidgets.close() + database.execSQL("DROP TABLE Widget_old") + } +} \ No newline at end of file diff --git a/core/ktx/src/main/java/de/mm20/launcher2/ktx/UUID.kt b/core/ktx/src/main/java/de/mm20/launcher2/ktx/UUID.kt new file mode 100644 index 00000000..d37db3f2 --- /dev/null +++ b/core/ktx/src/main/java/de/mm20/launcher2/ktx/UUID.kt @@ -0,0 +1,12 @@ +package de.mm20.launcher2.ktx + +import java.nio.ByteBuffer +import java.util.UUID + +fun UUID.toBytes(): ByteArray { + val bytes = ByteArray(16) + val buffer = ByteBuffer.wrap(bytes) + buffer.putLong(mostSignificantBits) + buffer.putLong(leastSignificantBits) + return buffer.array() +} \ No newline at end of file diff --git a/data/widgets/build.gradle.kts b/data/widgets/build.gradle.kts index 0bc8ac1e..f7fe9549 100644 --- a/data/widgets/build.gradle.kts +++ b/data/widgets/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.library") id("kotlin-android") + kotlin("plugin.serialization") } android { @@ -42,6 +43,8 @@ dependencies { implementation(libs.bundles.androidx.lifecycle) + implementation(libs.kotlinx.serialization.json) + implementation(libs.koin.android) implementation(project(":data:weather")) diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/AppWidget.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/AppWidget.kt new file mode 100644 index 00000000..79353483 --- /dev/null +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/AppWidget.kt @@ -0,0 +1,56 @@ +package de.mm20.launcher2.widgets + +import android.app.Activity +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.os.Build +import de.mm20.launcher2.database.entities.PartialWidgetEntity +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.util.UUID + +@Serializable +data class AppWidgetConfig( + val widgetId: Int, + val height: Int, +) + +data class AppWidget( + override val id: UUID, + val config: AppWidgetConfig, + val widgetProviderInfo: AppWidgetProviderInfo +) : Widget() { + override fun loadLabel(context: Context): String { + return widgetProviderInfo.loadLabel(context.packageManager) + } + + override fun toDatabaseEntity(): PartialWidgetEntity { + return PartialWidgetEntity( + id = id, + type = Type, + config = Json.encodeToString(config), + ) + } + + 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, + config.widgetId, + 0, + 0, + null + ) + } + + companion object { + const val Type = "app" + } +} \ No newline at end of file diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/CalendarWidget.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/CalendarWidget.kt new file mode 100644 index 00000000..f685277b --- /dev/null +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/CalendarWidget.kt @@ -0,0 +1,52 @@ +package de.mm20.launcher2.widgets + +import android.app.Activity +import android.appwidget.AppWidgetHost +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import de.mm20.launcher2.database.entities.PartialWidgetEntity +import de.mm20.launcher2.ktx.tryStartActivity +import kotlinx.serialization.Serializable +import java.util.UUID + +@Serializable +data class CalendarWidgetConfig( + val allDayEvents: Boolean = true, + val excludedCalendarIds: List = emptyList(), +) +data class CalendarWidget( + override val id: UUID, + val config: CalendarWidgetConfig = CalendarWidgetConfig(), +) : Widget() { + override fun loadLabel(context: Context): String { + return context.getString(R.string.widget_name_calendar) + } + + override fun toDatabaseEntity(): PartialWidgetEntity { + return PartialWidgetEntity( + id = id, + type = Type, + config = null, + ) + } + + 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) + } + + companion object { + const val Type = "calendar" + } +} \ No newline at end of file diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WeatherWIdget.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WeatherWIdget.kt new file mode 100644 index 00000000..9ee978e8 --- /dev/null +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WeatherWIdget.kt @@ -0,0 +1,55 @@ +package de.mm20.launcher2.widgets + +import android.app.Activity +import android.appwidget.AppWidgetHost +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import de.mm20.launcher2.database.entities.PartialWidgetEntity +import de.mm20.launcher2.ktx.tryStartActivity +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.util.UUID + + +@Serializable +data class WeatherWidgetConfig( + val showForecast: Boolean = true, +) + +data class WeatherWidget( + override val id: UUID, + val config: WeatherWidgetConfig = WeatherWidgetConfig(), +) : Widget() { + override fun loadLabel(context: Context): String { + return context.getString(R.string.widget_name_weather) + } + + override fun toDatabaseEntity(): PartialWidgetEntity { + return PartialWidgetEntity( + id = id, + type = Type, + config = Json.encodeToString(config), + ) + } + + 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) + } + + companion object { + const val Type = "weather" + } +} \ No newline at end of file diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt index 7f082b59..f579759f 100644 --- a/data/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/Widget.kt @@ -3,86 +3,78 @@ 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.database.entities.PartialWidgetEntity import de.mm20.launcher2.ktx.tryStartActivity +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import java.util.UUID sealed class Widget { + + abstract val id: UUID abstract fun loadLabel(context: Context): String - abstract fun toDatabaseEntity(position: Int = -1): WidgetEntity + fun toDatabaseEntity(position: Int, parentId: UUID? = null): WidgetEntity { + return toDatabaseEntity().let { + WidgetEntity( + id = it.id, + type = it.type, + config = it.config, + position = position, + parentId = parentId, + ) + } + } + abstract fun toDatabaseEntity(): PartialWidgetEntity + open val isConfigurable: Boolean = false open fun configure(context: Activity, appWidgetHost: AppWidgetHost) {} 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 - "favorites" -> FavoritesWidget - else -> null + return when (entity.type) { + WeatherWidget.Type -> { + val config: WeatherWidgetConfig = Json.decodeFromString(entity.config ?: "{}") + WeatherWidget(entity.id, config) } - } 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 - ) + MusicWidget.Type -> MusicWidget(entity.id) + CalendarWidget.Type -> { + val config: CalendarWidgetConfig = Json.decodeFromString(entity.config ?: "{}") + CalendarWidget(entity.id, config) + } + FavoritesWidget.Type -> FavoritesWidget(entity.id) + AppWidget.Type -> { + val config: AppWidgetConfig = Json.decodeFromString(entity.config ?: "{}") + AppWidget( + entity.id, + config, + widgetProviderInfo = AppWidgetManager.getInstance(context) + .getAppWidgetInfo(config.widgetId) + ) + } + + else -> null } } } } -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 = 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() { +data class MusicWidget( + override val id: UUID, +) : 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 fun toDatabaseEntity(): PartialWidgetEntity { + return PartialWidgetEntity( + id = id, + type = Type, + config = null ) } @@ -100,50 +92,27 @@ object MusicWidget : Widget() { ) 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) + companion object { + const val Type = "music" } } -object FavoritesWidget : Widget() { + + + +data class FavoritesWidget( + override val id: UUID, +) : Widget() { override fun loadLabel(context: Context): String { return context.getString(R.string.widget_name_favorites) } - override fun toDatabaseEntity(position: Int): WidgetEntity { - return WidgetEntity( - type = WidgetType.INTERNAL.value, - data = "favorites", - height = -1, - position = position + override fun toDatabaseEntity(): PartialWidgetEntity { + return PartialWidgetEntity( + id = id, + type = Type, + config = null, ) } @@ -161,44 +130,13 @@ object FavoritesWidget : Widget() { ) 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 - ) + companion object { + const val Type = "favorites" } } + enum class WidgetType(val value: String) { INTERNAL("internal"), THIRD_PARTY("3rdparty") diff --git a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt index 1ecb03aa..9c701f9c 100644 --- a/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt +++ b/data/widgets/src/main/java/de/mm20/launcher2/widgets/WidgetRepository.kt @@ -1,11 +1,7 @@ package de.mm20.launcher2.widgets -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProviderInfo import android.content.Context -import android.content.pm.LauncherApps -import android.content.pm.PackageManager -import android.util.Log +import androidx.room.withTransaction import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.entities.WidgetEntity @@ -16,22 +12,16 @@ import kotlinx.coroutines.flow.map import org.json.JSONArray import org.json.JSONException import java.io.File +import java.util.UUID interface WidgetRepository { - fun getWidgets(): Flow> - fun getInternalWidgets(): List + fun get(parent: UUID? = null, limit: Int = 100, offset: Int = 0): Flow> + fun update(widget: Widget) + fun create(widget: Widget, position: Int, parentId: UUID? = null) + fun delete(widget: Widget) + fun set(widgets: List, parentId: UUID? = null) - suspend fun getAppWidgets(): List - fun saveWidgets(widgets: List) - fun addWidget(widget: Widget, position: Int) - fun removeWidget(widget: Widget) - fun setWidgetHeight(widget: Widget, newHeight: Int) - fun isWeatherWidgetEnabled(): Flow - fun isMusicWidgetEnabled(): Flow - fun isCalendarWidgetEnabled(): Flow - fun isFavoritesWidgetEnabled(): Flow - - fun isFavoritesWidgetFirst(): Flow + fun exists(type: String): Flow suspend fun export(toDir: File) suspend fun import(fromDir: File) @@ -43,92 +33,60 @@ internal class WidgetRepositoryImpl( ) : WidgetRepository { private val scope = CoroutineScope(Job() + Dispatchers.Default) - - override fun getWidgets(): Flow> { - return database.widgetDao() - .getWidgets() - .map { it.mapNotNull { Widget.fromDatabaseEntity(context, it) } } - } - - override fun getInternalWidgets(): List { - return listOf(WeatherWidget, MusicWidget, CalendarWidget, FavoritesWidget) - } - - override suspend fun getAppWidgets(): List { - val appWidgetManager = AppWidgetManager.getInstance(context) - val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps - val profiles = launcherApps.profiles - val widgets = mutableListOf() - withContext(Dispatchers.IO) { - for (profile in profiles) { - widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile)) - } + override fun get(parent: UUID?, limit: Int, offset: Int): Flow> { + val dao = database.widgetDao() + return if (parent == null) { + dao.queryRoot(limit, offset) + } else { + dao.queryByParent(parent, limit, offset) + }.map { + it.mapNotNull { Widget.fromDatabaseEntity(context, it) } } - return widgets } - override fun saveWidgets(widgets: List) { + override fun update(widget: Widget) { + val dao = database.widgetDao() scope.launch { - withContext(Dispatchers.IO) { - database.widgetDao() - .updateWidgets(widgets.mapIndexed { i, widget -> widget.toDatabaseEntity(i) }) - } + dao.patch(widget.toDatabaseEntity()) } } - override fun addWidget(widget: Widget, position: Int) { + override fun create(widget: Widget, position: Int, parentId: UUID?) { + val dao = database.widgetDao() scope.launch { - withContext(Dispatchers.IO) { - database.widgetDao() - .insert(widget.toDatabaseEntity(position)) - } + val entity = widget.toDatabaseEntity(position = position, parentId = parentId) + dao.insert(entity) } } - override fun removeWidget(widget: Widget) { + override fun delete(widget: Widget) { + val dao = database.widgetDao() scope.launch { - withContext(Dispatchers.IO) { - val ent = widget.toDatabaseEntity() - database.widgetDao().deleteWidget( - ent.type, - ent.data - ) - } + dao.delete(widget.id) } } - override fun setWidgetHeight(widget: Widget, newHeight: Int) { + override fun set(widgets: List, parentId: UUID?) { + val dao = database.widgetDao() scope.launch { - withContext(Dispatchers.IO) { - val ent = widget.toDatabaseEntity() - database.widgetDao().updateHeight( - ent.type, - ent.data, - newHeight - ) + database.withTransaction { + if (parentId == null) { + dao.deleteRoot() + } else { + dao.deleteByParent(parentId) + } + dao.insert(widgets.mapIndexed { index, widget -> + widget.toDatabaseEntity(position = index, parentId = parentId) + }) } } } - override fun isWeatherWidgetEnabled(): Flow { - return database.widgetDao().exists("internal", "weather") + override fun exists(type: String): Flow { + val dao = database.widgetDao() + return dao.exists(type = type) } - override fun isMusicWidgetEnabled(): Flow { - return database.widgetDao().exists("internal", "music") - } - - override fun isCalendarWidgetEnabled(): Flow { - return database.widgetDao().exists("internal", "calendar") - } - - override fun isFavoritesWidgetEnabled(): Flow { - return database.widgetDao().exists("internal", "favorites") - } - - override fun isFavoritesWidgetFirst(): Flow { - return database.widgetDao().getFirst().map { it?.type == "internal" && it.data == "favorites" } - } override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { val dao = database.backupDao() @@ -140,13 +98,16 @@ internal class WidgetRepositoryImpl( if (widget.type != WidgetType.INTERNAL.value) continue jsonArray.put( jsonObjectOf( - "data" to widget.data, + "config" to widget.config, "position" to widget.position, + "type" to widget.type, + "id" to widget.id.toString(), + "parentId" to widget.parentId?.toString(), ) ) } - val file = File(toDir, "widgets.${page.toString().padStart(4, '0')}") + val file = File(toDir, "widgets2.${page.toString().padStart(4, '0')}") file.bufferedWriter().use { it.write(jsonArray.toString()) } @@ -158,7 +119,8 @@ internal class WidgetRepositoryImpl( val dao = database.backupDao() dao.wipeWidgets() - val files = fromDir.listFiles { _, name -> name.startsWith("widgets.") } ?: return@withContext + val files = + fromDir.listFiles { _, name -> name.startsWith("widgets2.") } ?: return@withContext for (file in files) { val widgets = mutableListOf() @@ -168,10 +130,11 @@ internal class WidgetRepositoryImpl( for (i in 0 until jsonArray.length()) { val json = jsonArray.getJSONObject(i) val entity = WidgetEntity( - type = WidgetType.INTERNAL.value, + type = json.getString("type"), position = json.getInt("position"), - data = json.getString("data"), - height = -1, + config = json.optString("config"), + id = json.getString("id").let { UUID.fromString(it) }, + parentId = json.optString("parentId").let { if (it.isEmpty()) null else UUID.fromString(it) } ) widgets.add(entity) } diff --git a/services/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt b/services/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt index 4ffd0d23..2a8c0d3a 100644 --- a/services/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt +++ b/services/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt @@ -3,7 +3,7 @@ package de.mm20.launcher2.backup enum class BackupComponent(val value: String) { Settings("settings"), Favorites("favorites"), - Widgets("widgets"), + Widgets("widgets2"), Customizations("customizations"), SearchActions("searchactions"); diff --git a/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt b/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt index 26ce6d23..25361f8b 100644 --- a/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt +++ b/services/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt @@ -188,7 +188,7 @@ class BackupManager( */ private const val BackupFormatMajor = 1 - private const val BackupFormatMinor = 5 + private const val BackupFormatMinor = 6 internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor" } } diff --git a/services/widgets/.gitignore b/services/widgets/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/services/widgets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/services/widgets/build.gradle.kts b/services/widgets/build.gradle.kts new file mode 100644 index 00000000..dfc5a2c8 --- /dev/null +++ b/services/widgets/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id("com.android.library") + id("kotlin-android") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } + namespace = "de.mm20.launcher2.services.widgets" +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.koin.android) + + implementation(project(":core:base")) + implementation(project(":core:i18n")) + implementation(project(":data:widgets")) + +} \ No newline at end of file diff --git a/services/widgets/consumer-rules.pro b/services/widgets/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/services/widgets/proguard-rules.pro b/services/widgets/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/services/widgets/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/services/widgets/src/main/AndroidManifest.xml b/services/widgets/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/services/widgets/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/BuiltInWidgetInfo.kt b/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/BuiltInWidgetInfo.kt new file mode 100644 index 00000000..c18f9ec7 --- /dev/null +++ b/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/BuiltInWidgetInfo.kt @@ -0,0 +1,7 @@ +package de.mm20.launcher2.services.widgets + + +data class BuiltInWidgetInfo( + val type: String, + val label: String, +) \ No newline at end of file diff --git a/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/Module.kt b/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/Module.kt new file mode 100644 index 00000000..06ca3c66 --- /dev/null +++ b/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/Module.kt @@ -0,0 +1,9 @@ +package de.mm20.launcher2.services.widgets + +import WidgetsService +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module + +val widgetsServiceModule = module { + single { WidgetsService(androidContext(), get()) } +} \ No newline at end of file diff --git a/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/WidgetsService.kt b/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/WidgetsService.kt new file mode 100644 index 00000000..615003e5 --- /dev/null +++ b/services/widgets/src/main/java/de/mm20/launcher2/services/widgets/WidgetsService.kt @@ -0,0 +1,71 @@ +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.content.pm.LauncherApps +import androidx.core.content.getSystemService +import de.mm20.launcher2.services.widgets.BuiltInWidgetInfo +import de.mm20.launcher2.services.widgets.R +import de.mm20.launcher2.widgets.CalendarWidget +import de.mm20.launcher2.widgets.FavoritesWidget +import de.mm20.launcher2.widgets.MusicWidget +import de.mm20.launcher2.widgets.WeatherWidget +import de.mm20.launcher2.widgets.Widget +import de.mm20.launcher2.widgets.WidgetRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import java.util.UUID + +class WidgetsService( + private val context: Context, + private val widgetRepository: WidgetRepository, +) { + suspend fun getAppWidgetProviders(): List = withContext(Dispatchers.IO) { + val appWidgetManager = AppWidgetManager.getInstance(context) + val launcherApps = context.getSystemService() ?: return@withContext emptyList() + val profiles = launcherApps.profiles + val widgets = mutableListOf() + for (profile in profiles) { + widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile)) + } + widgets + } + + fun getBuiltInWidgets(): List { + return listOf( + BuiltInWidgetInfo( + type = WeatherWidget.Type, + label = context.getString(R.string.widget_name_weather), + ), + BuiltInWidgetInfo( + type = MusicWidget.Type, + label = context.getString(R.string.widget_name_music), + ), + BuiltInWidgetInfo( + type = CalendarWidget.Type, + label = context.getString(R.string.widget_name_calendar), + ), + BuiltInWidgetInfo( + type = FavoritesWidget.Type, + label = context.getString(R.string.widget_name_favorites), + ), + ) + } + + fun addWidget(widget: Widget, position: Int, parentId: UUID? = null) { + widgetRepository.create(widget, position, parentId) + } + + fun getWidgets() = widgetRepository.get() + + fun isFavoritesWidgetFirst(): Flow { + return widgetRepository.get(limit = 1).map { + it.firstOrNull() is FavoritesWidget + } + } + + companion object { + const val AppWidgetHostId = 44203 + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index e6257b63..645a0e8f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,6 +30,9 @@ dependencyResolutionManagement { "kotlinx.collections.immutable" ) ) + version("kotlinx.serialization", "1.5.0") + library("kotlinx.serialization.json", "org.jetbrains.kotlinx", "kotlinx-serialization-json") + .versionRef("kotlinx.serialization") version("androidx.compose.compiler", "1.4.4") library("androidx.compose.runtime", "androidx.compose.runtime", "runtime") @@ -297,3 +300,4 @@ include(":libs:webdav") include(":libs:g-services") include(":libs:ms-services") include(":services:global-actions") +include(":services:widgets")