Refactor widgets

This commit is contained in:
MM20 2023-04-10 01:15:27 +02:00
parent 2054386179
commit 3cda75bc77
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
37 changed files with 1153 additions and 347 deletions

View File

@ -156,6 +156,7 @@ dependencies {
implementation(project(":core:database")) implementation(project(":core:database"))
implementation(project(":data:search-actions")) implementation(project(":data:search-actions"))
implementation(project(":services:global-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 // Uncomment this if you want annoying notifications in your debug builds yelling at you how terrible your code is
//debugImplementation(libs.leakcanary) //debugImplementation(libs.leakcanary)

View File

@ -30,6 +30,7 @@ import de.mm20.launcher2.permissions.permissionsModule
import de.mm20.launcher2.preferences.preferencesModule import de.mm20.launcher2.preferences.preferencesModule
import de.mm20.launcher2.searchactions.searchActionsModule import de.mm20.launcher2.searchactions.searchActionsModule
import de.mm20.launcher2.services.tags.servicesTagsModule import de.mm20.launcher2.services.tags.servicesTagsModule
import de.mm20.launcher2.services.widgets.widgetsServiceModule
import de.mm20.launcher2.weather.weatherModule import de.mm20.launcher2.weather.weatherModule
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
@ -81,6 +82,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
widgetsModule, widgetsModule,
wikipediaModule, wikipediaModule,
servicesTagsModule, servicesTagsModule,
widgetsServiceModule,
) )
) )
} }

View File

@ -150,4 +150,5 @@ dependencies {
implementation(project(":services:backup")) implementation(project(":services:backup"))
implementation(project(":data:search-actions")) implementation(project(":data:search-actions"))
implementation(project(":services:global-actions")) implementation(project(":services:global-actions"))
implementation(project(":services:widgets"))
} }

View File

@ -8,6 +8,7 @@ import de.mm20.launcher2.ui.component.ProvideIconShape
import de.mm20.launcher2.ui.locals.LocalCardStyle import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
import de.mm20.launcher2.ui.locals.LocalGridSettings import de.mm20.launcher2.ui.locals.LocalGridSettings
import de.mm20.launcher2.widgets.FavoritesWidget
import de.mm20.launcher2.widgets.WidgetRepository import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -35,7 +36,7 @@ fun ProvideSettings(
val favoritesEnabled by remember { val favoritesEnabled by remember {
combine( combine(
widgetRepository.isFavoritesWidgetEnabled(), widgetRepository.exists(FavoritesWidget.Type),
dataStore.data.map { it.favorites.enabled }, dataStore.data.map { it.favorites.enabled },
dataStore.data.map { it.clockWidget.favoritesPart }, dataStore.data.map { it.clockWidget.favoritesPart },
) { a, b, c -> a || b || c }.distinctUntilChanged() ) { a, b, c -> a || b || c }.distinctUntilChanged()

View File

@ -8,6 +8,7 @@ import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.WidgetRepository import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -36,7 +37,7 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
open val favorites: Flow<List<SavableSearchable>> = selectedTag.flatMapLatest { tag -> open val favorites: Flow<List<SavableSearchable>> = selectedTag.flatMapLatest { tag ->
if (tag == null) { if (tag == null) {
val columns = dataStore.data.map { it.grid.columnCount } 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 includeFrequentlyUsed = dataStore.data.map { it.favorites.frequentlyUsed }
val frequentlyUsedRows = dataStore.data.map { it.favorites.frequentlyUsedRows } val frequentlyUsedRows = dataStore.data.map { it.favorites.frequentlyUsedRows }

View File

@ -61,11 +61,13 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
import de.mm20.launcher2.widgets.CalendarWidget 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.FavoritesWidget
import de.mm20.launcher2.widgets.MusicWidget import de.mm20.launcher2.widgets.MusicWidget
import de.mm20.launcher2.widgets.WeatherWidget import de.mm20.launcher2.widgets.WeatherWidget
import de.mm20.launcher2.widgets.Widget import de.mm20.launcher2.widgets.Widget
import java.util.UUID
import kotlin.math.roundToInt import kotlin.math.roundToInt
class BindAndConfigureAppWidgetActivity : Activity() { class BindAndConfigureAppWidgetActivity : Activity() {
@ -191,9 +193,12 @@ private class BindAndConfigureAppWidgetContract(
) )
if (widgetId != null && widgetProviderInfo != null) { if (widgetId != null && widgetProviderInfo != null) {
return ExternalWidget( return AppWidget(
height = widgetProviderInfo.minHeight, id = UUID.randomUUID(),
widgetId = widgetId, config = AppWidgetConfig(
height = widgetProviderInfo.minHeight,
widgetId = widgetId,
),
widgetProviderInfo = widgetProviderInfo, widgetProviderInfo = widgetProviderInfo,
) )
} }
@ -277,7 +282,15 @@ fun WidgetPickerSheet(
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp), .padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
onClick = { 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() onDismiss()
}) { }) {
Row( Row(
@ -285,18 +298,18 @@ fun WidgetPickerSheet(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
imageVector = when (it) { imageVector = when (it.type) {
is WeatherWidget -> Icons.Rounded.LightMode WeatherWidget.Type -> Icons.Rounded.LightMode
is CalendarWidget -> Icons.Rounded.Today CalendarWidget.Type -> Icons.Rounded.Today
is MusicWidget -> Icons.Rounded.MusicNote MusicWidget.Type -> Icons.Rounded.MusicNote
is FavoritesWidget -> Icons.Rounded.Star FavoritesWidget.Type -> Icons.Rounded.Star
else -> Icons.Rounded.Widgets else -> Icons.Rounded.Widgets
}, },
contentDescription = null, contentDescription = null,
modifier = Modifier.padding(end = 16.dp) modifier = Modifier.padding(end = 16.dp)
) )
Text( Text(
text = it.loadLabel(context), text = it.label,
style = MaterialTheme.typography.titleSmall style = MaterialTheme.typography.titleSmall
) )
} }

View File

@ -1,14 +1,21 @@
package de.mm20.launcher2.ui.launcher.sheets package de.mm20.launcher2.ui.launcher.sheets
import WidgetsService
import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfo
import android.content.Context import android.content.Context
import android.content.pm.PackageManager 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.runtime.mutableStateOf
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import de.mm20.launcher2.ktx.normalize 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.Widget
import de.mm20.launcher2.widgets.WidgetRepository import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -24,7 +31,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
class WidgetPickerSheetVM( class WidgetPickerSheetVM(
private val widgetRepository: WidgetRepository, private val widgetsService: WidgetsService,
private val context: Context, private val context: Context,
) : ViewModel() { ) : ViewModel() {
@ -32,11 +39,11 @@ class WidgetPickerSheetVM(
val searchQuery = MutableStateFlow("") val searchQuery = MutableStateFlow("")
private val enabledWidgets = widgetRepository.getWidgets() private val enabledWidgets = widgetsService.getWidgets()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(100), emptyList()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(100), emptyList())
private val allBuiltInWidgets = enabledWidgets.map { w -> 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)) }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100))
val builtInWidgets = allBuiltInWidgets val builtInWidgets = allBuiltInWidgets
@ -45,13 +52,13 @@ class WidgetPickerSheetVM(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val normalizedQuery = query.normalize() val normalizedQuery = query.normalize()
widgets.filter { widgets.filter {
it.loadLabel(context).normalize().contains(normalizedQuery) it.label.normalize().contains(normalizedQuery)
} }
} }
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100)) }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100))
private val allAppWidgets = flow { private val allAppWidgets = flow {
val widgets = widgetRepository.getAppWidgets() val widgets = widgetsService.getAppWidgetProviders()
emit(widgets) emit(widgets)
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100)) }.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100))
@ -105,7 +112,7 @@ class WidgetPickerSheetVM(
fun pickWidget(widget: Widget) { fun pickWidget(widget: Widget) {
val position = enabledWidgets.value.size val position = enabledWidgets.value.size
widgetRepository.addWidget(widget, position) widgetsService.addWidget(widget, position)
} }
fun toggleGroup(group: String) { fun toggleGroup(group: String) {
@ -123,10 +130,17 @@ class WidgetPickerSheetVM(
} }
} }
} }
} }
data class AppWidgetGroup( data class AppWidgetGroup(
val appName: String, val appName: String,
val packageName: String, val packageName: String,
val widgets: List<AppWidgetProviderInfo> val widgets: List<AppWidgetProviderInfo>
)
data class BuiltInWidgetInfo(
val type: String,
@StringRes val label: Int,
val icon: ImageVector
) )

View File

@ -3,32 +3,19 @@ package de.mm20.launcher2.ui.launcher.widgets
import android.app.Activity import android.app.Activity
import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding 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.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -37,7 +24,6 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onPlaced 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.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.ktx.animateTo import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget import de.mm20.launcher2.widgets.AppWidget
import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
import de.mm20.launcher2.widgets.ExternalWidget
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch 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( Column(
modifier = modifier modifier = modifier
) { ) {
@ -109,7 +78,7 @@ fun WidgetColumn(
} }
val widgetsWithIndex = remember(widgets) { widgets.withIndex() } val widgetsWithIndex = remember(widgets) { widgets.withIndex() }
for ((i, widget) in widgetsWithIndex) { for ((i, widget) in widgetsWithIndex) {
key(if (widget is ExternalWidget) widget.widgetId else widget) { key(widget.id) {
var dragOffsetAfterSwap = remember<Float?> { null } var dragOffsetAfterSwap = remember<Float?> { null }
val offsetY = remember(widgets) { mutableStateOf(dragOffsetAfterSwap ?: 0f) } val offsetY = remember(widgets) { mutableStateOf(dragOffsetAfterSwap ?: 0f) }
@ -122,13 +91,13 @@ fun WidgetColumn(
appWidgetHost = widgetHost, appWidgetHost = widgetHost,
editMode = editMode, editMode = editMode,
onWidgetRemove = { onWidgetRemove = {
if (widget is ExternalWidget) { if (widget is AppWidget) {
widgetHost.deleteAppWidgetId(widget.widgetId) widgetHost.deleteAppWidgetId(widget.config.widgetId)
} }
viewModel.removeWidget(widget) viewModel.removeWidget(widget)
}, },
onWidgetResize = { onWidgetUpdate = {
viewModel.setWidgetHeight(widget, it) viewModel.updateWidget(it)
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher.widgets
import android.app.Activity import android.app.Activity
import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetHost
import android.util.Log
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.gestures.* 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.music.MusicWidget
import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidget
import de.mm20.launcher2.widgets.* import de.mm20.launcher2.widgets.*
import java.lang.Integer.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
@ -38,7 +38,7 @@ fun WidgetItem(
appWidgetHost: AppWidgetHost, appWidgetHost: AppWidgetHost,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
editMode: Boolean = false, editMode: Boolean = false,
onWidgetResize: (newHeight: Int) -> Unit = {}, onWidgetUpdate: (widget: Widget) -> Unit = {},
onWidgetRemove: () -> Unit = {}, onWidgetRemove: () -> Unit = {},
draggableState: DraggableState = rememberDraggableState {}, draggableState: DraggableState = rememberDraggableState {},
onDragStopped: () -> Unit = {} onDragStopped: () -> Unit = {}
@ -84,7 +84,7 @@ fun WidgetItem(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1 maxLines = 1
) )
if (widget is ExternalWidget) { if (widget is AppWidget) {
IconButton(onClick = { resizeMode = !resizeMode }) { IconButton(onClick = { resizeMode = !resizeMode }) {
Icon( Icon(
imageVector = Icons.Rounded.Edit, imageVector = Icons.Rounded.Edit,
@ -124,19 +124,19 @@ fun WidgetItem(
is FavoritesWidget -> { is FavoritesWidget -> {
FavoritesWidget() FavoritesWidget()
} }
is ExternalWidget -> { is AppWidget -> {
var height by remember(widget) { mutableStateOf(widget.height) } var dragDelta by remember { mutableStateOf(0) }
Column { Column {
ExternalWidget( ExternalWidget(
appWidgetHost = appWidgetHost, appWidgetHost = appWidgetHost,
widgetId = widget.widgetId, widgetId = widget.config.widgetId,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
height = height, height = widget.config.height + dragDelta,
) )
if (resizeMode) { if (resizeMode) {
val density = LocalDensity.current val density = LocalDensity.current
val drgStt = rememberDraggableState { val drgStt = rememberDraggableState {
height += (it / density.density).roundToInt() dragDelta += (it / density.density).roundToInt()
} }
Icon( Icon(
imageVector = Icons.Rounded.DragHandle, imageVector = Icons.Rounded.DragHandle,
@ -150,7 +150,12 @@ fun WidgetItem(
orientation = Orientation.Vertical, orientation = Orientation.Vertical,
startDragImmediately = true, startDragImmediately = true,
onDragStopped = { onDragStopped = {
onWidgetResize(height) onWidgetUpdate(widget.copy(
config = widget.config.copy(
height = widget.config.height + dragDelta
)
))
dragDelta = 0
} }
) )
) )

View File

@ -1,11 +1,9 @@
package de.mm20.launcher2.ui.launcher.widgets package de.mm20.launcher2.ui.launcher.widgets
import android.appwidget.AppWidgetManager import android.util.Log
import android.content.Context
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.widgets.ExternalWidget
import de.mm20.launcher2.widgets.Widget import de.mm20.launcher2.widgets.Widget
import de.mm20.launcher2.widgets.WidgetRepository import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -19,49 +17,28 @@ class WidgetsVM : ViewModel(), KoinComponent {
val editButton = dataStore.data.map { it.widgets.editButton }.asLiveData() 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) { fun removeWidget(widget: Widget) {
widgetRepository.removeWidget(widget) widgetRepository.delete(widget)
} }
fun setWidgetHeight(widget: Widget, newHeight: Int) { fun updateWidget(widget: Widget) {
widgetRepository.setWidgetHeight(widget, newHeight) widgetRepository.update(widget)
}
fun getAvailableBuiltInWidgets(): List<Widget> {
return widgetRepository.getInternalWidgets().filter {
widgets.value?.contains(it)?.not() ?: false
}
} }
fun moveUp(index: Int) { fun moveUp(index: Int) {
val widgets = widgets.value?.toMutableList() ?: return val widgets = widgets.value?.toMutableList() ?: return
val widget = widgets.removeAt(index) val widget = widgets.removeAt(index)
widgets.add(index - 1, widget) widgets.add(index - 1, widget)
widgetRepository.saveWidgets(widgets) widgetRepository.set(widgets)
} }
fun moveDown(index: Int) { fun moveDown(index: Int) {
val widgets = widgets.value?.toMutableList() ?: return val widgets = widgets.value?.toMutableList() ?: return
val widget = widgets.removeAt(index) val widget = widgets.removeAt(index)
widgets.add(index + 1, widget) widgets.add(index + 1, widget)
widgetRepository.saveWidgets(widgets) widgetRepository.set(widgets)
} }
} }

View File

@ -16,6 +16,7 @@ import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.WidgetRepository import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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 if (layout == ClockWidgetLayout.Horizontal) c - 2 else c
} }
}.collectAsState(0) }.collectAsState(0)
val excludeCalendar by remember { widgetRepository.isCalendarWidgetEnabled() }.collectAsState( val excludeCalendar by remember { widgetRepository.exists(CalendarWidget.Type) }.collectAsState(
true true
) )

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.launcher.widgets.favorites package de.mm20.launcher2.ui.launcher.widgets.favorites
import WidgetsService
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout import de.mm20.launcher2.preferences.Settings.ClockWidgetSettings.ClockWidgetLayout
import de.mm20.launcher2.ui.common.FavoritesVM 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.map
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.inject
class FavoritesWidgetVM : FavoritesVM() { class FavoritesWidgetVM : FavoritesVM() {
private val widgetsService: WidgetsService by inject()
override val tagsExpanded: Flow<Boolean> = dataStore.data.map { it.ui.widgetTagsMultiline } override val tagsExpanded: Flow<Boolean> = dataStore.data.map { it.ui.widgetTagsMultiline }
.shareIn(viewModelScope, SharingStarted.Lazily) .shareIn(viewModelScope, SharingStarted.Lazily)
private val isTopWidget = widgetRepository.isFavoritesWidgetFirst() private val isTopWidget = widgetsService.isFavoritesWidgetFirst()
private val clockWidgetFavSlots = dataStore.data.combine(isTopWidget) { data, isTop -> private val clockWidgetFavSlots = dataStore.data.combine(isTopWidget) { data, isTop ->
if (!isTop || !data.clockWidget.favoritesPart) 0 if (!isTop || !data.clockWidget.favoritesPart) 0
else { else {

View File

@ -4,6 +4,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore 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 de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -16,10 +20,10 @@ class WidgetSettingsScreenVM : ViewModel(), KoinComponent {
private val widgetRepository: WidgetRepository by inject() private val widgetRepository: WidgetRepository by inject()
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
val calendarWidget = widgetRepository.isCalendarWidgetEnabled().asLiveData() val calendarWidget = widgetRepository.exists(CalendarWidget.Type).asLiveData()
val musicWidget = widgetRepository.isMusicWidgetEnabled().asLiveData() val musicWidget = widgetRepository.exists(MusicWidget.Type).asLiveData()
val weatherWidget = widgetRepository.isWeatherWidgetEnabled().asLiveData() val weatherWidget = widgetRepository.exists(WeatherWidget.Type).asLiveData()
val favoritesWidget = widgetRepository.isFavoritesWidgetEnabled().asLiveData() val favoritesWidget = widgetRepository.exists(FavoritesWidget.Type).asLiveData()
val editButton = dataStore.data.map { it.widgets.editButton }.asLiveData() val editButton = dataStore.data.map { it.widgets.editButton }.asLiveData()
fun setEditButton(editButton: Boolean) { fun setEditButton(editButton: Boolean) {
viewModelScope.launch { viewModelScope.launch {

View File

@ -1,4 +1,8 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // 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 { buildscript {
repositories { repositories {

View File

@ -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')"
]
}
}

View File

@ -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_19_20
import de.mm20.launcher2.database.migrations.Migration_20_21 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_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_6_7
import de.mm20.launcher2.database.migrations.Migration_7_8 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_8_9
import de.mm20.launcher2.database.migrations.Migration_9_10 import de.mm20.launcher2.database.migrations.Migration_9_10
import de.mm20.launcher2.ktx.toBytes
import java.util.UUID
@Database( @Database(
entities = [ entities = [
@ -36,7 +39,7 @@ import de.mm20.launcher2.database.migrations.Migration_9_10
WidgetEntity::class, WidgetEntity::class,
CustomAttributeEntity::class, CustomAttributeEntity::class,
SearchActionEntity::class SearchActionEntity::class
], version = 22, exportSchema = true ], version = 23, exportSchema = true
) )
@TypeConverters(ComponentNameConverter::class, StringListConverter::class) @TypeConverters(ComponentNameConverter::class, StringListConverter::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -81,10 +84,15 @@ abstract class AppDatabase : RoomDatabase() {
) )
db.execSQL( db.execSQL(
"INSERT INTO Widget (type, data, height, position, label) VALUES " + "INSERT INTO Widget (`type`, `position`, `id`) VALUES " +
"('internal', 'weather', -1, 0, '${context.getString(R.string.widget_name_weather)}')," + "('weather', 0, ?)," +
"('internal', 'music', -1, 1, '${context.getString(R.string.widget_name_music)}')," + "('music', 1, ?)," +
"('internal', 'calendar', -1, 2, '${context.getString(R.string.widget_name_calendar)}');" "('calendar', 2, ?);",
arrayOf(
UUID.randomUUID().toBytes(),
UUID.randomUUID().toBytes(),
UUID.randomUUID().toBytes()
)
) )
} }
}) })
@ -104,7 +112,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration_18_19(), Migration_18_19(),
Migration_19_20(), Migration_19_20(),
Migration_20_21(), Migration_20_21(),
Migration_21_22() Migration_21_22(),
Migration_22_23(),
).build() ).build()
if (_instance == null) _instance = instance if (_instance == null) _instance = instance
return instance return instance

View File

@ -1,42 +1,54 @@
package de.mm20.launcher2.database package de.mm20.launcher2.database
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query 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.WidgetEntity
import de.mm20.launcher2.database.entities.PartialWidgetEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.util.UUID
@Dao @Dao
interface WidgetDao { interface WidgetDao {
@Query("SELECT * FROM Widget ORDER BY position ASC") @Query("SELECT * FROM Widget WHERE parentId IS NULL ORDER BY position ASC LIMIT :limit OFFSET :offset")
fun getWidgets(): Flow<List<WidgetEntity>> fun queryRoot(limit: Int, offset: Int): Flow<List<WidgetEntity>>
@Transaction @Query("SELECT * FROM Widget WHERE parentId = :parentId ORDER BY position ASC LIMIT :limit OFFSET :offset")
fun updateWidgets(widgets: List<WidgetEntity>) { fun queryByParent(parentId: UUID,limit: Int, offset: Int): Flow<List<WidgetEntity>>
deleteAll()
insertAll(widgets)
}
@Insert @Insert
fun insertAll(widgets: List<WidgetEntity>) suspend fun insert(widget: WidgetEntity)
@Insert @Insert
fun insert(widget: WidgetEntity) suspend fun insert(widgets: List<WidgetEntity>)
@Query("DELETE FROM Widget") @Update(entity = WidgetEntity::class)
fun deleteAll() suspend fun patch(widget: PartialWidgetEntity)
@Update(entity = WidgetEntity::class)
suspend fun patch(widgets: List<PartialWidgetEntity>)
@Query("DELETE FROM Widget WHERE data = :data AND type = :type") @Update
fun deleteWidget(type: String, data: String) suspend fun update(widget: WidgetEntity)
@Query("UPDATE Widget SET height = :newHeight WHERE data = :data AND type = :type") @Update
fun updateHeight(type: String, data: String, newHeight: Int) suspend fun update(widgets: List<WidgetEntity>)
@Query("SELECT EXISTS(SELECT 1 FROM Widget WHERE type = :type AND data = :data)") @Query("DELETE FROM Widget WHERE id = :id")
fun exists(type: String, data: String) : Flow<Boolean> suspend fun delete(id: UUID)
@Query("DELETE FROM WIDGET WHERE id IN (:ids)")
suspend fun delete(ids: List<UUID>)
@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<Boolean>
@Query("SELECT * FROM Widget ORDER BY position ASC LIMIT 1")
fun getFirst() : Flow<WidgetEntity?>
} }

View File

@ -2,14 +2,23 @@ package de.mm20.launcher2.database.entities
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.util.UUID
@Entity(tableName = "Widget") @Entity(tableName = "Widget")
data class WidgetEntity( data class WidgetEntity(
val type: String, val type: String,
var data: String, var config: String?,
var height: Int,
var position: Int, var position: Int,
val label: String = "", @PrimaryKey val id: UUID,
@PrimaryKey(autoGenerate = true) val id: Int? = null val parentId: UUID? = null,
)
/**
* Partial entity for updating and deleting
*/
data class PartialWidgetEntity(
val type: String,
var config: String?,
@PrimaryKey val id: UUID,
) )

View File

@ -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")
}
}

View File

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

View File

@ -1,6 +1,7 @@
plugins { plugins {
id("com.android.library") id("com.android.library")
id("kotlin-android") id("kotlin-android")
kotlin("plugin.serialization")
} }
android { android {
@ -42,6 +43,8 @@ dependencies {
implementation(libs.bundles.androidx.lifecycle) implementation(libs.bundles.androidx.lifecycle)
implementation(libs.kotlinx.serialization.json)
implementation(libs.koin.android) implementation(libs.koin.android)
implementation(project(":data:weather")) implementation(project(":data:weather"))

View File

@ -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"
}
}

View File

@ -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<Long> = 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"
}
}

View File

@ -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"
}
}

View File

@ -3,86 +3,78 @@ package de.mm20.launcher2.widgets
import android.app.Activity import android.app.Activity
import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import de.mm20.launcher2.database.entities.WidgetEntity import de.mm20.launcher2.database.entities.WidgetEntity
import de.mm20.launcher2.database.entities.PartialWidgetEntity
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import java.util.UUID
sealed class Widget { sealed class Widget {
abstract val id: UUID
abstract fun loadLabel(context: Context): String 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 val isConfigurable: Boolean = false
open fun configure(context: Activity, appWidgetHost: AppWidgetHost) {} open fun configure(context: Activity, appWidgetHost: AppWidgetHost) {}
companion object { companion object {
fun fromDatabaseEntity(context: Context, entity: WidgetEntity): Widget? { fun fromDatabaseEntity(context: Context, entity: WidgetEntity): Widget? {
if (entity.type == WidgetType.INTERNAL.value) { return when (entity.type) {
return when (entity.data) { WeatherWidget.Type -> {
"weather" -> WeatherWidget val config: WeatherWidgetConfig = Json.decodeFromString(entity.config ?: "{}")
"music" -> MusicWidget WeatherWidget(entity.id, config)
"calendar" -> CalendarWidget
"favorites" -> FavoritesWidget
else -> null
} }
} else { MusicWidget.Type -> MusicWidget(entity.id)
val widgetId = entity.data.toIntOrNull() ?: return null CalendarWidget.Type -> {
val widgetInfo = val config: CalendarWidgetConfig = Json.decodeFromString(entity.config ?: "{}")
AppWidgetManager.getInstance(context).getAppWidgetInfo(widgetId) ?: return null CalendarWidget(entity.id, config)
return ExternalWidget( }
height = entity.height, FavoritesWidget.Type -> FavoritesWidget(entity.id)
widgetId = widgetId, AppWidget.Type -> {
widgetProviderInfo = widgetInfo val config: AppWidgetConfig = Json.decodeFromString(entity.config ?: "{}")
) AppWidget(
entity.id,
config,
widgetProviderInfo = AppWidgetManager.getInstance(context)
.getAppWidgetInfo(config.widgetId)
)
}
else -> null
} }
} }
} }
} }
object WeatherWidget : Widget() { data class MusicWidget(
override fun loadLabel(context: Context): String { override val id: UUID,
return context.getString(R.string.widget_name_weather) ) : Widget() {
}
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() {
override fun loadLabel(context: Context): String { override fun loadLabel(context: Context): String {
return context.getString(R.string.widget_name_music) return context.getString(R.string.widget_name_music)
} }
override fun toDatabaseEntity(position: Int): WidgetEntity { override fun toDatabaseEntity(): PartialWidgetEntity {
return WidgetEntity( return PartialWidgetEntity(
type = WidgetType.INTERNAL.value, id = id,
data = "music", type = Type,
height = -1, config = null
position = position
) )
} }
@ -100,50 +92,27 @@ object MusicWidget : Widget() {
) )
context.tryStartActivity(intent) context.tryStartActivity(intent)
} }
}
companion object {
object CalendarWidget : Widget() { const val Type = "music"
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)
} }
} }
object FavoritesWidget : Widget() {
data class FavoritesWidget(
override val id: UUID,
) : Widget() {
override fun loadLabel(context: Context): String { override fun loadLabel(context: Context): String {
return context.getString(R.string.widget_name_favorites) return context.getString(R.string.widget_name_favorites)
} }
override fun toDatabaseEntity(position: Int): WidgetEntity { override fun toDatabaseEntity(): PartialWidgetEntity {
return WidgetEntity( return PartialWidgetEntity(
type = WidgetType.INTERNAL.value, id = id,
data = "favorites", type = Type,
height = -1, config = null,
position = position
) )
} }
@ -161,44 +130,13 @@ object FavoritesWidget : Widget() {
) )
context.tryStartActivity(intent) context.tryStartActivity(intent)
} }
}
companion object {
class ExternalWidget( const val Type = "favorites"
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(val value: String) { enum class WidgetType(val value: String) {
INTERNAL("internal"), INTERNAL("internal"),
THIRD_PARTY("3rdparty") THIRD_PARTY("3rdparty")

View File

@ -1,11 +1,7 @@
package de.mm20.launcher2.widgets package de.mm20.launcher2.widgets
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context import android.content.Context
import android.content.pm.LauncherApps import androidx.room.withTransaction
import android.content.pm.PackageManager
import android.util.Log
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.WidgetEntity import de.mm20.launcher2.database.entities.WidgetEntity
@ -16,22 +12,16 @@ import kotlinx.coroutines.flow.map
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import java.io.File import java.io.File
import java.util.UUID
interface WidgetRepository { interface WidgetRepository {
fun getWidgets(): Flow<List<Widget>> fun get(parent: UUID? = null, limit: Int = 100, offset: Int = 0): Flow<List<Widget>>
fun getInternalWidgets(): List<Widget> fun update(widget: Widget)
fun create(widget: Widget, position: Int, parentId: UUID? = null)
fun delete(widget: Widget)
fun set(widgets: List<Widget>, parentId: UUID? = null)
suspend fun getAppWidgets(): List<AppWidgetProviderInfo> fun exists(type: String): Flow<Boolean>
fun saveWidgets(widgets: List<Widget>)
fun addWidget(widget: Widget, position: Int)
fun removeWidget(widget: Widget)
fun setWidgetHeight(widget: Widget, newHeight: Int)
fun isWeatherWidgetEnabled(): Flow<Boolean>
fun isMusicWidgetEnabled(): Flow<Boolean>
fun isCalendarWidgetEnabled(): Flow<Boolean>
fun isFavoritesWidgetEnabled(): Flow<Boolean>
fun isFavoritesWidgetFirst(): Flow<Boolean>
suspend fun export(toDir: File) suspend fun export(toDir: File)
suspend fun import(fromDir: File) suspend fun import(fromDir: File)
@ -43,92 +33,60 @@ internal class WidgetRepositoryImpl(
) : WidgetRepository { ) : WidgetRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default) private val scope = CoroutineScope(Job() + Dispatchers.Default)
override fun get(parent: UUID?, limit: Int, offset: Int): Flow<List<Widget>> {
override fun getWidgets(): Flow<List<Widget>> { val dao = database.widgetDao()
return database.widgetDao() return if (parent == null) {
.getWidgets() dao.queryRoot(limit, offset)
.map { it.mapNotNull { Widget.fromDatabaseEntity(context, it) } } } else {
} dao.queryByParent(parent, limit, offset)
}.map {
override fun getInternalWidgets(): List<Widget> { it.mapNotNull { Widget.fromDatabaseEntity(context, it) }
return listOf(WeatherWidget, MusicWidget, CalendarWidget, FavoritesWidget)
}
override suspend fun getAppWidgets(): List<AppWidgetProviderInfo> {
val appWidgetManager = AppWidgetManager.getInstance(context)
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val profiles = launcherApps.profiles
val widgets = mutableListOf<AppWidgetProviderInfo>()
withContext(Dispatchers.IO) {
for (profile in profiles) {
widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile))
}
} }
return widgets
} }
override fun saveWidgets(widgets: List<Widget>) { override fun update(widget: Widget) {
val dao = database.widgetDao()
scope.launch { scope.launch {
withContext(Dispatchers.IO) { dao.patch(widget.toDatabaseEntity())
database.widgetDao()
.updateWidgets(widgets.mapIndexed { i, widget -> widget.toDatabaseEntity(i) })
}
} }
} }
override fun addWidget(widget: Widget, position: Int) { override fun create(widget: Widget, position: Int, parentId: UUID?) {
val dao = database.widgetDao()
scope.launch { scope.launch {
withContext(Dispatchers.IO) { val entity = widget.toDatabaseEntity(position = position, parentId = parentId)
database.widgetDao() dao.insert(entity)
.insert(widget.toDatabaseEntity(position))
}
} }
} }
override fun removeWidget(widget: Widget) { override fun delete(widget: Widget) {
val dao = database.widgetDao()
scope.launch { scope.launch {
withContext(Dispatchers.IO) { dao.delete(widget.id)
val ent = widget.toDatabaseEntity()
database.widgetDao().deleteWidget(
ent.type,
ent.data
)
}
} }
} }
override fun setWidgetHeight(widget: Widget, newHeight: Int) { override fun set(widgets: List<Widget>, parentId: UUID?) {
val dao = database.widgetDao()
scope.launch { scope.launch {
withContext(Dispatchers.IO) { database.withTransaction {
val ent = widget.toDatabaseEntity() if (parentId == null) {
database.widgetDao().updateHeight( dao.deleteRoot()
ent.type, } else {
ent.data, dao.deleteByParent(parentId)
newHeight }
) dao.insert(widgets.mapIndexed { index, widget ->
widget.toDatabaseEntity(position = index, parentId = parentId)
})
} }
} }
} }
override fun isWeatherWidgetEnabled(): Flow<Boolean> { override fun exists(type: String): Flow<Boolean> {
return database.widgetDao().exists("internal", "weather") val dao = database.widgetDao()
return dao.exists(type = type)
} }
override fun isMusicWidgetEnabled(): Flow<Boolean> {
return database.widgetDao().exists("internal", "music")
}
override fun isCalendarWidgetEnabled(): Flow<Boolean> {
return database.widgetDao().exists("internal", "calendar")
}
override fun isFavoritesWidgetEnabled(): Flow<Boolean> {
return database.widgetDao().exists("internal", "favorites")
}
override fun isFavoritesWidgetFirst(): Flow<Boolean> {
return database.widgetDao().getFirst().map { it?.type == "internal" && it.data == "favorites" }
}
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
val dao = database.backupDao() val dao = database.backupDao()
@ -140,13 +98,16 @@ internal class WidgetRepositoryImpl(
if (widget.type != WidgetType.INTERNAL.value) continue if (widget.type != WidgetType.INTERNAL.value) continue
jsonArray.put( jsonArray.put(
jsonObjectOf( jsonObjectOf(
"data" to widget.data, "config" to widget.config,
"position" to widget.position, "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 { file.bufferedWriter().use {
it.write(jsonArray.toString()) it.write(jsonArray.toString())
} }
@ -158,7 +119,8 @@ internal class WidgetRepositoryImpl(
val dao = database.backupDao() val dao = database.backupDao()
dao.wipeWidgets() 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) { for (file in files) {
val widgets = mutableListOf<WidgetEntity>() val widgets = mutableListOf<WidgetEntity>()
@ -168,10 +130,11 @@ internal class WidgetRepositoryImpl(
for (i in 0 until jsonArray.length()) { for (i in 0 until jsonArray.length()) {
val json = jsonArray.getJSONObject(i) val json = jsonArray.getJSONObject(i)
val entity = WidgetEntity( val entity = WidgetEntity(
type = WidgetType.INTERNAL.value, type = json.getString("type"),
position = json.getInt("position"), position = json.getInt("position"),
data = json.getString("data"), config = json.optString("config"),
height = -1, id = json.getString("id").let { UUID.fromString(it) },
parentId = json.optString("parentId").let { if (it.isEmpty()) null else UUID.fromString(it) }
) )
widgets.add(entity) widgets.add(entity)
} }

View File

@ -3,7 +3,7 @@ package de.mm20.launcher2.backup
enum class BackupComponent(val value: String) { enum class BackupComponent(val value: String) {
Settings("settings"), Settings("settings"),
Favorites("favorites"), Favorites("favorites"),
Widgets("widgets"), Widgets("widgets2"),
Customizations("customizations"), Customizations("customizations"),
SearchActions("searchactions"); SearchActions("searchactions");

View File

@ -188,7 +188,7 @@ class BackupManager(
*/ */
private const val BackupFormatMajor = 1 private const val BackupFormatMajor = 1
private const val BackupFormatMinor = 5 private const val BackupFormatMinor = 6
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor" internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
} }
} }

1
services/widgets/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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"))
}

View File

21
services/widgets/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -0,0 +1,7 @@
package de.mm20.launcher2.services.widgets
data class BuiltInWidgetInfo(
val type: String,
val label: String,
)

View File

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

View File

@ -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<AppWidgetProviderInfo> = withContext(Dispatchers.IO) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val launcherApps = context.getSystemService<LauncherApps>() ?: return@withContext emptyList()
val profiles = launcherApps.profiles
val widgets = mutableListOf<AppWidgetProviderInfo>()
for (profile in profiles) {
widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile))
}
widgets
}
fun getBuiltInWidgets(): List<BuiltInWidgetInfo> {
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<Boolean> {
return widgetRepository.get(limit = 1).map {
it.firstOrNull() is FavoritesWidget
}
}
companion object {
const val AppWidgetHostId = 44203
}
}

View File

@ -30,6 +30,9 @@ dependencyResolutionManagement {
"kotlinx.collections.immutable" "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") version("androidx.compose.compiler", "1.4.4")
library("androidx.compose.runtime", "androidx.compose.runtime", "runtime") library("androidx.compose.runtime", "androidx.compose.runtime", "runtime")
@ -297,3 +300,4 @@ include(":libs:webdav")
include(":libs:g-services") include(":libs:g-services")
include(":libs:ms-services") include(":libs:ms-services")
include(":services:global-actions") include(":services:global-actions")
include(":services:widgets")