Refactor widgets
This commit is contained in:
parent
2054386179
commit
3cda75bc77
@ -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)
|
||||||
|
|||||||
@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"))
|
||||||
}
|
}
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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?>
|
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
)
|
)
|
||||||
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
12
core/ktx/src/main/java/de/mm20/launcher2/ktx/UUID.kt
Normal file
12
core/ktx/src/main/java/de/mm20/launcher2/ktx/UUID.kt
Normal 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()
|
||||||
|
}
|
||||||
@ -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"))
|
||||||
|
|||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
||||||
|
|
||||||
|
|||||||
@ -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
1
services/widgets/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
46
services/widgets/build.gradle.kts
Normal file
46
services/widgets/build.gradle.kts
Normal 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"))
|
||||||
|
|
||||||
|
}
|
||||||
0
services/widgets/consumer-rules.pro
Normal file
0
services/widgets/consumer-rules.pro
Normal file
21
services/widgets/proguard-rules.pro
vendored
Normal file
21
services/widgets/proguard-rules.pro
vendored
Normal 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
|
||||||
4
services/widgets/src/main/AndroidManifest.xml
Normal file
4
services/widgets/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package de.mm20.launcher2.services.widgets
|
||||||
|
|
||||||
|
|
||||||
|
data class BuiltInWidgetInfo(
|
||||||
|
val type: String,
|
||||||
|
val label: String,
|
||||||
|
)
|
||||||
@ -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()) }
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user