Add calendar plugins
This commit is contained in:
parent
c7987aabcd
commit
7479c7f401
2
.idea/inspectionProfiles/Project_Default.xml
generated
2
.idea/inspectionProfiles/Project_Default.xml
generated
@ -1,7 +1,7 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="AndroidLintNewerVersionAvailable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="AndroidLintUnsafeImplicitIntentLaunch" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
|
||||
@ -51,14 +51,14 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
|
||||
val frequentlyUsedRows = it.second.frequentlyUsedRows
|
||||
|
||||
val pinned = favoritesService.getFavorites(
|
||||
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
|
||||
excludeTypes = if (excludeCalendar) listOf("calendar", "tag", "plugin.calendar") else listOf("tag"),
|
||||
minPinnedLevel = PinnedLevel.AutomaticallySorted,
|
||||
limit = 10 * columns,
|
||||
)
|
||||
if (includeFrequentlyUsed) {
|
||||
emitAll(pinned.flatMapLatest { pinned ->
|
||||
favoritesService.getFavorites(
|
||||
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
|
||||
excludeTypes = if (excludeCalendar) listOf("calendar", "tag", "plugin.calendar") else listOf("tag"),
|
||||
maxPinnedLevel = PinnedLevel.FrequentlyUsed,
|
||||
minPinnedLevel = PinnedLevel.FrequentlyUsed,
|
||||
limit = frequentlyUsedRows * columns - pinned.size % columns,
|
||||
|
||||
@ -409,15 +409,15 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
|
||||
val missingCalendarPermission = combine(
|
||||
permissionsManager.hasPermission(PermissionGroup.Calendar),
|
||||
calendarSearchSettings.enabled,
|
||||
) { perm, enabled -> !perm && enabled }
|
||||
calendarSearchSettings.providers,
|
||||
) { perm, providers -> !perm && providers.contains("local") }
|
||||
|
||||
fun requestCalendarPermission(context: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(context, PermissionGroup.Calendar)
|
||||
}
|
||||
|
||||
fun disableCalendarSearch() {
|
||||
calendarSearchSettings.setEnabled(false)
|
||||
calendarSearchSettings.setProviderEnabled("local", false)
|
||||
}
|
||||
|
||||
val missingContactsPermission = combine(
|
||||
|
||||
@ -8,10 +8,10 @@ import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandIn
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@ -19,21 +19,19 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.CheckCircle
|
||||
import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material.icons.rounded.Notes
|
||||
import androidx.compose.material.icons.rounded.OpenInNew
|
||||
import androidx.compose.material.icons.rounded.People
|
||||
import androidx.compose.material.icons.rounded.Place
|
||||
import androidx.compose.material.icons.rounded.RadioButtonUnchecked
|
||||
import androidx.compose.material.icons.rounded.Schedule
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.StarOutline
|
||||
import androidx.compose.material.icons.rounded.Tune
|
||||
import androidx.compose.material.icons.rounded.Visibility
|
||||
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@ -49,8 +47,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.roundToIntRect
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
||||
@ -63,8 +59,6 @@ import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
import de.mm20.launcher2.ui.locals.LocalDarkTheme
|
||||
import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled
|
||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
|
||||
import kotlinx.coroutines.launch
|
||||
import palettes.TonalPalette
|
||||
|
||||
@Composable
|
||||
@ -99,33 +93,53 @@ fun CalendarItem(
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
) {
|
||||
Box(
|
||||
Icon(
|
||||
when (calendar.isCompleted) {
|
||||
true -> Icons.Rounded.CheckCircle
|
||||
false -> Icons.Rounded.RadioButtonUnchecked
|
||||
null -> Icons.Rounded.Circle
|
||||
},
|
||||
null,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 14.dp)
|
||||
.padding(horizontal = 14.dp, vertical = 20.dp)
|
||||
.size(24.dp)
|
||||
.sharedBounds(
|
||||
.sharedElement(
|
||||
rememberSharedContentState("color"),
|
||||
this@AnimatedContent
|
||||
)
|
||||
.background(eventColor, MaterialTheme.shapes.extraSmall)
|
||||
),
|
||||
tint = eventColor
|
||||
)
|
||||
|
||||
Text(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp, end = 16.dp)
|
||||
.sharedBounds(
|
||||
rememberSharedContentState("label"),
|
||||
this@AnimatedContent
|
||||
),
|
||||
text = calendar.labelOverride ?: calendar.label,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.sharedBounds(
|
||||
rememberSharedContentState("label"),
|
||||
this@AnimatedContent
|
||||
),
|
||||
text = calendar.labelOverride ?: calendar.label,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (calendar.calendarName != null) {
|
||||
Text(
|
||||
modifier = Modifier.animateEnterExit(
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically()
|
||||
),
|
||||
text = calendar.calendarName!!,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = eventColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth().padding(bottom = 8.dp, end = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
@ -146,7 +160,8 @@ fun CalendarItem(
|
||||
if (!calendar.description.isNullOrBlank()) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth().padding(bottom = 8.dp, end = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
@ -165,7 +180,8 @@ fun CalendarItem(
|
||||
if (calendar.attendees.isNotEmpty()) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth().padding(bottom = 8.dp, end = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
@ -261,15 +277,21 @@ fun CalendarItem(
|
||||
modifier = modifier
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Box(
|
||||
Icon(
|
||||
when (calendar.isCompleted) {
|
||||
true -> Icons.Rounded.CheckCircle
|
||||
false -> Icons.Rounded.RadioButtonUnchecked
|
||||
null -> Icons.Rounded.Circle
|
||||
},
|
||||
null,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(20.dp)
|
||||
.sharedBounds(
|
||||
.sharedElement(
|
||||
rememberSharedContentState("color"),
|
||||
this@AnimatedContent
|
||||
)
|
||||
.background(eventColor, MaterialTheme.shapes.extraSmall)
|
||||
),
|
||||
tint = eventColor
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
@ -281,13 +303,16 @@ fun CalendarItem(
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 2.dp)
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp)
|
||||
.sharedBounds(
|
||||
rememberSharedContentState("date"),
|
||||
this@AnimatedContent
|
||||
),
|
||||
text = calendar.getSummary(context),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@ -327,6 +352,24 @@ fun CalendarItemGridPopup(
|
||||
}
|
||||
|
||||
private fun CalendarEvent.formatTime(context: Context): String {
|
||||
val startTime = startTime
|
||||
if (startTime == null) {
|
||||
if (allDay) {
|
||||
return DateUtils.formatDateRange(
|
||||
context,
|
||||
endTime,
|
||||
endTime,
|
||||
DateUtils.FORMAT_SHOW_DATE
|
||||
)
|
||||
}
|
||||
return DateUtils.formatDateRange(
|
||||
context,
|
||||
endTime,
|
||||
endTime,
|
||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME
|
||||
)
|
||||
}
|
||||
|
||||
if (allDay) return DateUtils.formatDateRange(
|
||||
context,
|
||||
startTime,
|
||||
@ -343,6 +386,39 @@ private fun CalendarEvent.formatTime(context: Context): String {
|
||||
}
|
||||
|
||||
private fun CalendarEvent.getSummary(context: Context): String {
|
||||
val startTime = startTime
|
||||
if (startTime == null) {
|
||||
val isToday = DateUtils.isToday(endTime)
|
||||
if (isToday) {
|
||||
if (allDay) {
|
||||
return "Due today"
|
||||
}
|
||||
return "Due ${
|
||||
DateUtils.formatDateTime(
|
||||
context,
|
||||
endTime,
|
||||
DateUtils.FORMAT_SHOW_TIME
|
||||
)
|
||||
}"
|
||||
}
|
||||
if (allDay) {
|
||||
return "Due ${
|
||||
DateUtils.formatDateTime(
|
||||
context,
|
||||
endTime,
|
||||
DateUtils.FORMAT_SHOW_DATE
|
||||
)
|
||||
}"
|
||||
}
|
||||
return "Due ${
|
||||
DateUtils.formatDateTime(
|
||||
context,
|
||||
endTime,
|
||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME
|
||||
)
|
||||
}"
|
||||
}
|
||||
|
||||
val isToday =
|
||||
DateUtils.isToday(startTime) && DateUtils.isToday(endTime)
|
||||
return if (isToday) {
|
||||
|
||||
@ -34,7 +34,6 @@ import androidx.compose.material.icons.rounded.Link
|
||||
import androidx.compose.material.icons.rounded.LinkOff
|
||||
import androidx.compose.material.icons.rounded.OpenInNew
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -64,7 +63,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.isUnspecified
|
||||
import androidx.core.net.toUri
|
||||
import de.mm20.launcher2.calendar.CalendarRepository
|
||||
import de.mm20.launcher2.calendar.UserCalendar
|
||||
import de.mm20.launcher2.calendar.providers.CalendarList
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
@ -75,7 +74,6 @@ import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||
import de.mm20.launcher2.ui.component.DragResizeHandle
|
||||
import de.mm20.launcher2.ui.component.LargeMessage
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.component.ResizeAxis
|
||||
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
|
||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
|
||||
@ -91,6 +89,7 @@ import de.mm20.launcher2.widgets.MusicWidget
|
||||
import de.mm20.launcher2.widgets.NotesWidget
|
||||
import de.mm20.launcher2.widgets.WeatherWidget
|
||||
import de.mm20.launcher2.widgets.Widget
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.androidx.compose.get
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
@ -504,18 +503,16 @@ fun ColumnScope.ConfigureCalendarWidget(
|
||||
) {
|
||||
val calendarRepository: CalendarRepository = get()
|
||||
val permissionsManager: PermissionsManager = get()
|
||||
var calendars by remember { mutableStateOf(emptyList<UserCalendar>()) }
|
||||
var ready by remember { mutableStateOf(false) }
|
||||
val calendars by remember {
|
||||
calendarRepository.getCalendars().map {
|
||||
it.sortedBy { it.name }
|
||||
}
|
||||
}.collectAsState(null)
|
||||
|
||||
val hasPermission by remember {
|
||||
permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
}.collectAsState(true)
|
||||
|
||||
LaunchedEffect(hasPermission) {
|
||||
calendars = calendarRepository.getCalendars().sortedBy { it.name }
|
||||
ready = true
|
||||
}
|
||||
|
||||
OutlinedCard {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@ -537,26 +534,29 @@ fun ColumnScope.ConfigureCalendarWidget(
|
||||
text = stringResource(R.string.preference_calendar_calendars)
|
||||
)
|
||||
val context = LocalLifecycleOwner.current as AppCompatActivity
|
||||
if (calendars.isNotEmpty()) {
|
||||
val excludedCalendars = remember(widget.config) {
|
||||
widget.config.excludedCalendarIds ?: widget.config.legacyExcludedCalendarIds?.map { "local:$it" } ?: emptyList()
|
||||
}
|
||||
if (calendars?.isNotEmpty() == true) {
|
||||
OutlinedCard {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
for ((i, calendar) in calendars.withIndex()) {
|
||||
if (i > 0) Divider()
|
||||
for ((i, calendar) in calendars!!.withIndex()) {
|
||||
if (i > 0) HorizontalDivider()
|
||||
CheckboxPreference(
|
||||
title = calendar.name,
|
||||
summary = calendar.owner,
|
||||
iconPadding = false,
|
||||
value = !widget.config.excludedCalendarIds.contains(calendar.id),
|
||||
value = excludedCalendars.contains(calendar.id) != true,
|
||||
onValueChanged = {
|
||||
onWidgetUpdated(
|
||||
widget.copy(
|
||||
config = widget.config.copy(
|
||||
excludedCalendarIds = if (it) {
|
||||
widget.config.excludedCalendarIds - calendar.id
|
||||
excludedCalendars - calendar.id
|
||||
} else {
|
||||
widget.config.excludedCalendarIds + calendar.id
|
||||
excludedCalendars + calendar.id
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -572,7 +572,7 @@ fun ColumnScope.ConfigureCalendarWidget(
|
||||
text = stringResource(R.string.missing_permission_calendar_widget_settings),
|
||||
onClick = { permissionsManager.requestPermission(context, PermissionGroup.Calendar) },
|
||||
)
|
||||
} else if (ready) {
|
||||
} else if (calendars != null) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
|
||||
@ -43,7 +43,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
||||
val calendarEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
||||
val pinnedCalendarEvents =
|
||||
favoritesService.getFavorites(
|
||||
includeTypes = listOf("calendar"),
|
||||
includeTypes = listOf("calendar", "plugin.calendar"),
|
||||
minPinnedLevel = PinnedLevel.AutomaticallySorted,
|
||||
).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
val nextEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
||||
@ -67,10 +67,10 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
||||
field = value
|
||||
val dates = value.flatMap {
|
||||
val startDate =
|
||||
Instant.ofEpochMilli(it.startTime).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||
it.startTime?.let { Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate() }
|
||||
val endDate =
|
||||
Instant.ofEpochMilli(it.endTime).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||
return@flatMap listOf(
|
||||
return@flatMap listOfNotNull(
|
||||
startDate,
|
||||
endDate
|
||||
)
|
||||
@ -136,14 +136,14 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
||||
val dayStart = max(now, date.atStartOfDay().toEpochSecond(offset) * 1000)
|
||||
val dayEnd = date.plusDays(1).atStartOfDay().toEpochSecond(offset) * 1000
|
||||
var events = upcomingEvents.filter {
|
||||
it.endTime >= dayStart && it.startTime < dayEnd
|
||||
it.endTime >= dayStart && (it.startTime ?: it.endTime) < dayEnd
|
||||
}
|
||||
|
||||
if (!showRunningPastDayEvents) {
|
||||
val totalCount = events.size
|
||||
|
||||
events = events.filter {
|
||||
it.startTime >= date.atStartOfDay().toEpochSecond(offset) * 1000 ||
|
||||
(it.startTime != null && it.startTime!! >= date.atStartOfDay().toEpochSecond(offset) * 1000) ||
|
||||
it.endTime < date.atStartOfDay().plusDays(1).toEpochSecond(offset) * 1000
|
||||
}
|
||||
|
||||
@ -169,14 +169,14 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
||||
from = System.currentTimeMillis(),
|
||||
to = System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000L,
|
||||
excludeAllDayEvents = !config.allDayEvents,
|
||||
excludeCalendars = config.excludedCalendarIds,
|
||||
excludeCalendars = config.excludedCalendarIds ?: config.legacyExcludedCalendarIds?.map { "local:$it" } ?: emptyList(),
|
||||
).collectLatest { events ->
|
||||
searchableRepository.getKeys(
|
||||
includeTypes = listOf("calendar"),
|
||||
includeTypes = listOf("calendar", "plugin.calendar"),
|
||||
maxVisibility = VisibilityLevel.SearchOnly,
|
||||
limit = 9999,
|
||||
).collectLatest { hidden ->
|
||||
upcomingEvents = events.filter { !hidden.contains(it.key) }
|
||||
upcomingEvents = events.filter { !hidden.contains(it.key) }.sortedBy { it.startTime ?: it.endTime }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ import de.mm20.launcher2.ui.settings.about.AboutSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarSearchSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.colorscheme.ThemesSettingsScreen
|
||||
@ -180,6 +181,9 @@ class SettingsActivity : BaseActivity() {
|
||||
composable("settings/search/files") {
|
||||
FileSearchSettingsScreen()
|
||||
}
|
||||
composable("settings/search/calendar") {
|
||||
CalendarSearchSettingsScreen()
|
||||
}
|
||||
composable("settings/search/searchactions") {
|
||||
SearchActionsSettingsScreen()
|
||||
}
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
package de.mm20.launcher2.ui.settings.calendarsearch
|
||||
|
||||
import android.app.PendingIntent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ErrorOutline
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
|
||||
import de.mm20.launcher2.plugin.PluginState
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.Banner
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun CalendarSearchSettingsScreen() {
|
||||
val viewModel: CalendarSearchSettingsScreenVM = viewModel()
|
||||
val context = LocalContext.current
|
||||
|
||||
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsState(null)
|
||||
val plugins by viewModel.availablePlugins.collectAsState(emptyList())
|
||||
val enabledProviders by viewModel.enabledProviders.collectAsState(emptySet())
|
||||
|
||||
PreferenceScreen(title = stringResource(R.string.preference_search_calendar)) {
|
||||
item {
|
||||
AnimatedVisibility(hasCalendarPermission == false) {
|
||||
MissingPermissionBanner(
|
||||
text = stringResource(R.string.missing_permission_calendar_search_settings),
|
||||
onClick = {
|
||||
viewModel.requestCalendarPermission(context as AppCompatActivity)
|
||||
},
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.preference_search_calendar),
|
||||
summary = stringResource(R.string.preference_search_calendar_summary),
|
||||
value = enabledProviders.contains("local") && hasCalendarPermission == true,
|
||||
onValueChanged = {
|
||||
viewModel.setProviderEnabled("local", it)
|
||||
},
|
||||
enabled = hasCalendarPermission == true
|
||||
)
|
||||
for (plugin in plugins) {
|
||||
val state = plugin.state
|
||||
if (state is PluginState.SetupRequired) {
|
||||
Banner(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
text = state.message ?: stringResource(id = R.string.plugin_state_setup_required),
|
||||
icon = Icons.Rounded.ErrorOutline,
|
||||
primaryAction = {
|
||||
TextButton(onClick = {
|
||||
try {
|
||||
state.setupActivity.sendWithBackgroundPermission(context)
|
||||
} catch (e: PendingIntent.CanceledException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(id = R.string.plugin_action_setup))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
SwitchPreference(
|
||||
title = plugin.plugin.label,
|
||||
enabled = state is PluginState.Ready,
|
||||
summary = (state as? PluginState.Ready)?.text
|
||||
?: (state as? PluginState.SetupRequired)?.message
|
||||
?: plugin.plugin.description,
|
||||
value = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready,
|
||||
onValueChanged = {
|
||||
viewModel.setProviderEnabled(plugin.plugin.authority, it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package de.mm20.launcher2.ui.settings.calendarsearch
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.ViewModel
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
import de.mm20.launcher2.plugins.PluginService
|
||||
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class CalendarSearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
private val settings: CalendarSearchSettings by inject()
|
||||
private val pluginService: PluginService by inject()
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
|
||||
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
|
||||
val availablePlugins = pluginService.getPluginsWithState(
|
||||
type = PluginType.Calendar,
|
||||
enabled = true,
|
||||
)
|
||||
|
||||
val enabledProviders = settings.enabledProviders
|
||||
|
||||
fun setProviderEnabled(providerId: String, enabled: Boolean) {
|
||||
settings.setProviderEnabled(providerId, enabled)
|
||||
}
|
||||
|
||||
fun requestCalendarPermission(activity: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.accounts.AccountType
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
|
||||
import de.mm20.launcher2.plugin.PluginState
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.Banner
|
||||
@ -196,7 +197,7 @@ fun FileSearchSettingsScreen() {
|
||||
primaryAction = {
|
||||
TextButton(onClick = {
|
||||
try {
|
||||
state.setupActivity.send()
|
||||
state.setupActivity.sendWithBackgroundPermission(context)
|
||||
} catch (e: PendingIntent.CanceledException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
|
||||
import de.mm20.launcher2.plugin.PluginState
|
||||
import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
||||
import de.mm20.launcher2.ui.R
|
||||
@ -41,6 +42,7 @@ fun LocationsSettingsScreen() {
|
||||
val viewModel: LocationsSettingsScreenVM = viewModel()
|
||||
|
||||
val navController = LocalNavController.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val osmLocations by viewModel.osmLocations.collectAsState()
|
||||
val imperialUnits by viewModel.imperialUnits.collectAsState()
|
||||
@ -85,7 +87,7 @@ fun LocationsSettingsScreen() {
|
||||
primaryAction = {
|
||||
TextButton(onClick = {
|
||||
try {
|
||||
state.setupActivity.send()
|
||||
state.setupActivity.sendWithBackgroundPermission(context)
|
||||
} catch (e: PendingIntent.CanceledException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material.icons.rounded.LightMode
|
||||
import androidx.compose.material.icons.rounded.Place
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.Today
|
||||
import androidx.compose.material.icons.rounded.Verified
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -243,6 +244,7 @@ fun PluginSettingsScreen(pluginId: String) {
|
||||
PluginType.FileSearch -> Icons.AutoMirrored.Rounded.InsertDriveFile
|
||||
PluginType.Weather -> Icons.Rounded.LightMode
|
||||
PluginType.LocationSearch -> Icons.Rounded.Place
|
||||
PluginType.Calendar -> Icons.Rounded.Today
|
||||
},
|
||||
null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
@ -253,6 +255,7 @@ fun PluginSettingsScreen(pluginId: String) {
|
||||
PluginType.FileSearch -> stringResource(R.string.plugin_type_filesearch)
|
||||
PluginType.Weather -> stringResource(R.string.plugin_type_weather)
|
||||
PluginType.LocationSearch -> stringResource(R.string.plugin_type_locationsearch)
|
||||
PluginType.Calendar -> stringResource(R.string.plugin_type_calendar)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
|
||||
@ -116,28 +116,13 @@ fun SearchSettingsScreen() {
|
||||
enabled = hasContactsPermission == true
|
||||
)
|
||||
|
||||
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsStateWithLifecycle(
|
||||
null
|
||||
)
|
||||
AnimatedVisibility(hasCalendarPermission == false) {
|
||||
MissingPermissionBanner(
|
||||
text = stringResource(R.string.missing_permission_calendar_search_settings),
|
||||
onClick = {
|
||||
viewModel.requestCalendarPermission(context as AppCompatActivity)
|
||||
},
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
val calendar by viewModel.calendar.collectAsStateWithLifecycle(null)
|
||||
SwitchPreference(
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_search_calendar),
|
||||
summary = stringResource(R.string.preference_search_calendar_summary),
|
||||
icon = Icons.Rounded.Today,
|
||||
value = calendar == true && hasCalendarPermission == true,
|
||||
onValueChanged = {
|
||||
viewModel.setCalendar(it)
|
||||
onClick = {
|
||||
navController?.navigate("settings/search/calendar")
|
||||
},
|
||||
enabled = hasCalendarPermission == true
|
||||
)
|
||||
|
||||
val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(
|
||||
|
||||
@ -61,19 +61,6 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
permissionsManager.requestPermission(activity, PermissionGroup.Contacts)
|
||||
}
|
||||
|
||||
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
val calendar = calendarSearchSettings.enabled
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
|
||||
fun setCalendar(calendar: Boolean) {
|
||||
calendarSearchSettings.setEnabled(calendar)
|
||||
}
|
||||
|
||||
fun requestCalendarPermission(activity: AppCompatActivity) {
|
||||
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
|
||||
}
|
||||
|
||||
val calculator = calculatorSearchSettings.enabled
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
|
||||
import de.mm20.launcher2.plugin.PluginState
|
||||
import de.mm20.launcher2.ui.BuildConfig
|
||||
import de.mm20.launcher2.ui.R
|
||||
@ -58,7 +59,7 @@ fun WeatherIntegrationSettingsScreen() {
|
||||
primaryAction = {
|
||||
TextButton(onClick = {
|
||||
try {
|
||||
state.setupActivity.send()
|
||||
state.setupActivity.sendWithBackgroundPermission(context)
|
||||
} catch (e: PendingIntent.CanceledException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ abstract class QueryPluginApi<TQuery, TResult>(
|
||||
val uri = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(pluginAuthority)
|
||||
.path(LocationPluginContract.Paths.Search)
|
||||
.path(SearchPluginContract.Paths.Search)
|
||||
.appendQueryParameters(query)
|
||||
.appendQueryParameter(
|
||||
SearchPluginContract.Params.AllowNetwork,
|
||||
|
||||
@ -6,14 +6,17 @@ import de.mm20.launcher2.icons.StaticLauncherIcon
|
||||
import de.mm20.launcher2.icons.TextLayer
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
interface CalendarEvent: SavableSearchable {
|
||||
interface CalendarEvent : SavableSearchable {
|
||||
val color: Int?
|
||||
val startTime: Long
|
||||
val startTime: Long?
|
||||
val endTime: Long
|
||||
val allDay: Boolean
|
||||
val description: String?
|
||||
val calendarName: String?
|
||||
val location: String?
|
||||
val attendees: List<String>
|
||||
val isCompleted: Boolean?
|
||||
get() = null
|
||||
|
||||
|
||||
override val preferDetailsOverLaunch: Boolean
|
||||
@ -24,7 +27,7 @@ interface CalendarEvent: SavableSearchable {
|
||||
val df = SimpleDateFormat("dd")
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TextLayer(
|
||||
text = df.format(startTime),
|
||||
text = df.format(startTime ?: endTime),
|
||||
color = color ?: 0,
|
||||
),
|
||||
backgroundLayer = ColorLayer(color ?: 0)
|
||||
|
||||
@ -848,6 +848,7 @@
|
||||
<string name="search_filter_online">Online results</string>
|
||||
<string name="search_filter_apps">Apps</string>
|
||||
<string name="plugin_type_locationsearch">Places search</string>
|
||||
<string name="plugin_type_calendar">Calendar</string>
|
||||
<string name="preference_default_filter">Default filter</string>
|
||||
<string name="preference_default_filter_summary">Customize the default filter for searches</string>
|
||||
<string name="preference_filter_bar">Show filter bar</string>
|
||||
|
||||
@ -53,7 +53,9 @@ data class LauncherSettingsData internal constructor(
|
||||
|
||||
val contactSearchEnabled: Boolean = true,
|
||||
|
||||
@Deprecated("Use calendarSearchProviders `local` instead")
|
||||
val calendarSearchEnabled: Boolean = true,
|
||||
val calendarSearchProviders: Set<String> = setOf("local"),
|
||||
|
||||
val shortcutSearchEnabled: Boolean = true,
|
||||
|
||||
|
||||
@ -5,13 +5,20 @@ import kotlinx.coroutines.flow.map
|
||||
|
||||
class CalendarSearchSettings internal constructor(
|
||||
private val dataStore: LauncherDataStore,
|
||||
){
|
||||
val enabled
|
||||
get() = dataStore.data.map { it.calendarSearchEnabled }
|
||||
) {
|
||||
val providers
|
||||
get() = dataStore.data.map { it.calendarSearchProviders }
|
||||
|
||||
fun setEnabled(enabled: Boolean) {
|
||||
val enabledProviders
|
||||
get() = dataStore.data.map { it.calendarSearchProviders }
|
||||
|
||||
fun setProviderEnabled(provider: String, enabled: Boolean) {
|
||||
dataStore.update {
|
||||
it.copy(calendarSearchEnabled = enabled)
|
||||
if (enabled) {
|
||||
it.copy(calendarSearchProviders = it.calendarSearchProviders + provider)
|
||||
} else {
|
||||
it.copy(calendarSearchProviders = it.calendarSearchProviders - provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
package de.mm20.launcher2.plugin.contracts
|
||||
|
||||
abstract class CalendarPluginContract {
|
||||
object EventColumns: Columns() {
|
||||
object EventColumns : Columns() {
|
||||
/**
|
||||
* The unique ID of the event.
|
||||
*/
|
||||
@ -17,6 +17,11 @@ abstract class CalendarPluginContract {
|
||||
*/
|
||||
val Description = column<String>("description")
|
||||
|
||||
/**
|
||||
* The calendar name of the event.
|
||||
*/
|
||||
val CalendarName = column<String>("calendar_name")
|
||||
|
||||
/**
|
||||
* The location of the event.
|
||||
*/
|
||||
@ -33,9 +38,9 @@ abstract class CalendarPluginContract {
|
||||
val EndTime = column<Long>("end_time")
|
||||
|
||||
/**
|
||||
* Whether the event is an all-day event.
|
||||
* Whether the times should include times or truncate to dates.
|
||||
*/
|
||||
val AllDay = column<Boolean>("all_day")
|
||||
val IncludeTime = column<Boolean>("include_time")
|
||||
|
||||
/**
|
||||
* The color of the event.
|
||||
@ -46,5 +51,34 @@ abstract class CalendarPluginContract {
|
||||
* The attendees of the event.
|
||||
*/
|
||||
val Attendees = column<List<String>>("attendees")
|
||||
|
||||
/**
|
||||
* The URI of the event.
|
||||
*/
|
||||
val Uri = column<String>("uri")
|
||||
|
||||
/**
|
||||
* Whether the event is a task and if so, whether it is completed.
|
||||
*/
|
||||
val IsCompleted = column<Boolean>("completed")
|
||||
}
|
||||
|
||||
object CalendarListColumns : Columns() {
|
||||
val Id = column<String>("id")
|
||||
val Name = column<String>("name")
|
||||
val Color = column<Int>("color")
|
||||
val AccountName = column<String>("account_name")
|
||||
}
|
||||
|
||||
|
||||
object Params {
|
||||
const val Query = "query"
|
||||
const val StartTime = "start"
|
||||
const val EndTime = "end"
|
||||
const val Exclude = "exclude"
|
||||
}
|
||||
|
||||
object Paths {
|
||||
const val CalendarLists = "calendar_lists"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package de.mm20.launcher2.search.calendar
|
||||
|
||||
data class CalendarQuery(
|
||||
val query: String?,
|
||||
val start: Long?,
|
||||
val end: Long?,
|
||||
/**
|
||||
* List of calendar list ids that should be excluded from the search results.
|
||||
*/
|
||||
val excludedCalendars: List<String>,
|
||||
)
|
||||
@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.plugin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@ -1,42 +1,51 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.provider.CalendarContract
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarProvider
|
||||
import de.mm20.launcher2.calendar.providers.CalendarList
|
||||
import de.mm20.launcher2.calendar.providers.CalendarProvider
|
||||
import de.mm20.launcher2.calendar.providers.PluginCalendarProvider
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Calendar
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
interface CalendarRepository : SearchableRepository<CalendarEvent> {
|
||||
fun findMany(
|
||||
from: Long = System.currentTimeMillis(),
|
||||
to: Long = from + 14 * 24 * 60 * 60 * 1000L,
|
||||
excludeCalendars: List<Long> = emptyList(),
|
||||
excludeCalendars: List<String> = emptyList(),
|
||||
excludeAllDayEvents: Boolean = false,
|
||||
limit: Int = 999,
|
||||
): Flow<ImmutableList<CalendarEvent>>
|
||||
|
||||
suspend fun getCalendars(): List<UserCalendar>
|
||||
fun getCalendars(providerId: String? = null): Flow<List<CalendarList>>
|
||||
}
|
||||
|
||||
internal class CalendarRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager,
|
||||
private val pluginRepository: PluginRepository,
|
||||
private val settings: CalendarSearchSettings,
|
||||
) : CalendarRepository {
|
||||
|
||||
@ -48,48 +57,62 @@ internal class CalendarRepositoryImpl(
|
||||
}
|
||||
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
val enabled = settings.enabled
|
||||
val providerIds = settings.providers
|
||||
|
||||
return hasPermission.combine(enabled) { a, b -> a && b }
|
||||
.map {
|
||||
if (it) {
|
||||
val now = System.currentTimeMillis()
|
||||
queryCalendarEvents(
|
||||
query,
|
||||
intervalStart = now,
|
||||
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
|
||||
).toImmutableList()
|
||||
} else {
|
||||
persistentListOf()
|
||||
return combineTransform(hasPermission, providerIds) { perm, providerIds ->
|
||||
val providers = providerIds.mapNotNull {
|
||||
when (it) {
|
||||
"local" -> if (perm) AndroidCalendarProvider(context) else null
|
||||
else -> PluginCalendarProvider(context, it)
|
||||
}
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
emitAll(
|
||||
queryCalendarEvents(
|
||||
query = query,
|
||||
excludeAllDayEvents = false,
|
||||
providers = providers,
|
||||
intervalStart = now,
|
||||
intervalEnd = now + 730.days.inWholeMilliseconds,
|
||||
allowNetwork = allowNetwork,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findMany(
|
||||
from: Long,
|
||||
to: Long,
|
||||
excludeCalendars: List<Long>,
|
||||
excludeCalendars: List<String>,
|
||||
excludeAllDayEvents: Boolean,
|
||||
limit: Int,
|
||||
) = channelFlow<ImmutableList<CalendarEvent>> {
|
||||
): Flow<ImmutableList<CalendarEvent>> {
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
hasPermission.collectLatest {
|
||||
if (it) {
|
||||
val events = withContext(Dispatchers.IO) {
|
||||
queryCalendarEvents(
|
||||
query = null,
|
||||
intervalStart = from,
|
||||
intervalEnd = to,
|
||||
limit = limit,
|
||||
excludeAllDayEvents = excludeAllDayEvents,
|
||||
excludeCalendars = excludeCalendars
|
||||
)
|
||||
}
|
||||
send(events.toImmutableList())
|
||||
} else {
|
||||
send(persistentListOf())
|
||||
val plugins = pluginRepository.findMany(
|
||||
type = PluginType.Calendar,
|
||||
enabled = true,
|
||||
)
|
||||
return combineTransform(hasPermission, plugins) { perm, plugins ->
|
||||
val providers = buildList {
|
||||
if (perm) add(AndroidCalendarProvider(context)) else null
|
||||
addAll(
|
||||
plugins.map {
|
||||
PluginCalendarProvider(context, it.authority)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
emitAll(
|
||||
queryCalendarEvents(
|
||||
query = null,
|
||||
intervalStart = from,
|
||||
intervalEnd = to,
|
||||
excludeAllDayEvents = excludeAllDayEvents,
|
||||
excludeCalendars = excludeCalendars,
|
||||
providers = providers,
|
||||
allowNetwork = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,124 +120,70 @@ internal class CalendarRepositoryImpl(
|
||||
query: String?,
|
||||
intervalStart: Long,
|
||||
intervalEnd: Long,
|
||||
limit: Int = 10,
|
||||
excludeAllDayEvents: Boolean = false,
|
||||
excludeCalendars: List<Long> = emptyList(),
|
||||
): List<AndroidCalendarEvent> {
|
||||
val results = withContext(Dispatchers.IO) {
|
||||
val results = mutableListOf<AndroidCalendarEvent>()
|
||||
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
|
||||
ContentUris.appendId(builder, intervalStart)
|
||||
ContentUris.appendId(builder, intervalEnd)
|
||||
val uri = builder.build()
|
||||
val projection = arrayOf(
|
||||
CalendarContract.Instances.EVENT_ID,
|
||||
CalendarContract.Instances.TITLE,
|
||||
CalendarContract.Instances.BEGIN,
|
||||
CalendarContract.Instances.END,
|
||||
CalendarContract.Instances.ALL_DAY,
|
||||
CalendarContract.Instances.DISPLAY_COLOR,
|
||||
CalendarContract.Instances.EVENT_LOCATION,
|
||||
CalendarContract.Instances.CALENDAR_ID,
|
||||
CalendarContract.Instances.DESCRIPTION
|
||||
)
|
||||
val selection = mutableListOf<String>()
|
||||
if (query != null) selection.add("${CalendarContract.Instances.TITLE} LIKE ?")
|
||||
if (excludeCalendars.isNotEmpty()) selection.add("${CalendarContract.Instances.CALENDAR_ID} NOT IN (${excludeCalendars.joinToString()})")
|
||||
if (excludeAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0")
|
||||
val selArgs = if (query != null) arrayOf("%$query%") else null
|
||||
val sort =
|
||||
"${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else ""
|
||||
val cursor = context.contentResolver.query(
|
||||
uri,
|
||||
projection,
|
||||
selection.joinToString(separator = " AND "),
|
||||
selArgs,
|
||||
sort
|
||||
) ?: return@withContext mutableListOf()
|
||||
val proj = arrayOf(
|
||||
CalendarContract.Attendees.EVENT_ID,
|
||||
CalendarContract.Attendees.ATTENDEE_NAME,
|
||||
CalendarContract.Attendees.ATTENDEE_EMAIL
|
||||
)
|
||||
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
|
||||
while (cursor.moveToNext()) {
|
||||
val sel = "${CalendarContract.Attendees.EVENT_ID} = ${cursor.getLong(0)}"
|
||||
val cur = context.contentResolver.query(
|
||||
CalendarContract.Attendees.CONTENT_URI,
|
||||
proj, sel, null, s
|
||||
) ?: return@withContext mutableListOf()
|
||||
val attendees = mutableListOf<String>()
|
||||
while (cur.moveToNext()) {
|
||||
attendees.add(
|
||||
cur.getStringOrNull(1).takeUnless { it.isNullOrBlank() }
|
||||
?: cur.getStringOrNull(2)
|
||||
?: continue
|
||||
excludeCalendars: List<String> = emptyList(),
|
||||
allowNetwork: Boolean = false,
|
||||
providers: List<CalendarProvider>,
|
||||
): Flow<ImmutableList<CalendarEvent>> = flow {
|
||||
supervisorScope {
|
||||
val result = MutableStateFlow(persistentListOf<CalendarEvent>())
|
||||
|
||||
for (provider in providers) {
|
||||
launch {
|
||||
val r = provider.search(
|
||||
query,
|
||||
from = intervalStart,
|
||||
to = intervalEnd,
|
||||
excludedCalendars = excludeCalendars.mapNotNull {
|
||||
val (namespace, id) = it.split(":")
|
||||
if (namespace == provider.namespace) id else null
|
||||
},
|
||||
excludeAllDayEvents = excludeAllDayEvents,
|
||||
allowNetwork = allowNetwork,
|
||||
)
|
||||
result.update {
|
||||
(it + r).toPersistentList()
|
||||
}
|
||||
}
|
||||
cur.close()
|
||||
val allday = cursor.getInt(4) > 0
|
||||
val begin = cursor.getLong(2)
|
||||
|
||||
val tzOffset = if (allday) {
|
||||
Calendar.getInstance().timeZone.getOffset(begin)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val event = AndroidCalendarEvent(
|
||||
label = cursor.getStringOrNull(1) ?: "",
|
||||
id = cursor.getLong(0),
|
||||
color = cursor.getInt(5),
|
||||
startTime = begin - tzOffset,
|
||||
endTime = cursor.getLong(3) - tzOffset - if (allday) 1 else 0,
|
||||
allDay = allday,
|
||||
location = cursor.getStringOrNull(6) ?: "",
|
||||
attendees = attendees,
|
||||
description = cursor.getStringOrNull(8)
|
||||
?: "",
|
||||
calendar = cursor.getLong(7)
|
||||
)
|
||||
results.add(event)
|
||||
}
|
||||
cursor.close()
|
||||
return@withContext results
|
||||
emitAll(result)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
override suspend fun getCalendars(): List<UserCalendar> {
|
||||
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Calendar)) return emptyList()
|
||||
return withContext(Dispatchers.IO) {
|
||||
val calendars = mutableListOf<UserCalendar>()
|
||||
val uri = CalendarContract.Calendars.CONTENT_URI
|
||||
val proj = arrayOf(
|
||||
CalendarContract.Calendars._ID,
|
||||
CalendarContract.Calendars.NAME,
|
||||
CalendarContract.Calendars.ACCOUNT_NAME,
|
||||
CalendarContract.Calendars.CALENDAR_COLOR,
|
||||
CalendarContract.Calendars.VISIBLE,
|
||||
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
|
||||
override fun getCalendars(providerId: String?): Flow<List<CalendarList>> {
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
|
||||
val providers: Flow<List<CalendarProvider>> = if (providerId != null) {
|
||||
when(providerId) {
|
||||
"local" -> hasPermission.map { if (it) listOf(AndroidCalendarProvider(context)) else emptyList() }
|
||||
else -> pluginRepository.get(providerId).map { if (it?.enabled == true) listOf(PluginCalendarProvider(context, providerId)) else emptyList() }
|
||||
}
|
||||
} else {
|
||||
val plugins = pluginRepository.findMany(
|
||||
type = PluginType.Calendar,
|
||||
enabled = true,
|
||||
)
|
||||
val cursor = context.contentResolver.query(uri, proj, null, null, null)
|
||||
?: return@withContext emptyList()
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
calendars.add(
|
||||
UserCalendar(
|
||||
id = cursor.getLong(0),
|
||||
name = cursor.getStringOrNull(5) ?: cursor.getStringOrNull(1) ?: "",
|
||||
owner = cursor.getStringOrNull(2) ?: "",
|
||||
color = cursor.getInt(3)
|
||||
)
|
||||
)
|
||||
} catch (e: NullPointerException) {
|
||||
continue
|
||||
combine(hasPermission, plugins) { perm, plugins ->
|
||||
buildList {
|
||||
if (perm) add(AndroidCalendarProvider(context))
|
||||
addAll(plugins.map { PluginCalendarProvider(context, it.authority) })
|
||||
}
|
||||
}
|
||||
cursor.close()
|
||||
calendars.sortBy { it.owner }
|
||||
return@withContext calendars
|
||||
}
|
||||
|
||||
return providers.transform { providers ->
|
||||
supervisorScope {
|
||||
val result = MutableStateFlow(emptyList<CalendarList>())
|
||||
for (provider in providers) {
|
||||
launch {
|
||||
val r = provider.getCalendarLists()
|
||||
result.update { it + r }
|
||||
}
|
||||
}
|
||||
emitAll(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,31 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.provider.CalendarContract
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarEvent
|
||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarProvider
|
||||
import de.mm20.launcher2.calendar.providers.PluginCalendarEvent
|
||||
import de.mm20.launcher2.calendar.providers.PluginCalendarProvider
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.UpdateResult
|
||||
import de.mm20.launcher2.search.asUpdateResult
|
||||
import de.mm20.launcher2.serialization.Json
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
class CalendarEventSerializer: SearchableSerializer {
|
||||
class AndroidCalendarEventSerializer: SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as AndroidCalendarEvent
|
||||
val json = JSONObject()
|
||||
@ -22,84 +34,117 @@ class CalendarEventSerializer: SearchableSerializer {
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = "calendar"
|
||||
get() = AndroidCalendarEvent.Domain
|
||||
}
|
||||
|
||||
class CalendarEventDeserializer(val context: Context): SearchableDeserializer {
|
||||
class AndroidCalendarEventDeserializer(val context: Context): SearchableDeserializer {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
|
||||
val json = JSONObject(serialized)
|
||||
val id = json.getLong("id")
|
||||
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
|
||||
ContentUris.appendId(builder, System.currentTimeMillis())
|
||||
ContentUris.appendId(builder, System.currentTimeMillis() + 63072000000L)
|
||||
val uri = builder.build()
|
||||
val projection = arrayOf(
|
||||
CalendarContract.Instances.EVENT_ID,
|
||||
CalendarContract.Instances.TITLE,
|
||||
CalendarContract.Instances.BEGIN,
|
||||
CalendarContract.Instances.END,
|
||||
CalendarContract.Instances.ALL_DAY,
|
||||
CalendarContract.Instances.DISPLAY_COLOR,
|
||||
CalendarContract.Instances.EVENT_LOCATION,
|
||||
CalendarContract.Instances.CALENDAR_ID,
|
||||
CalendarContract.Instances.DESCRIPTION
|
||||
)
|
||||
val selection = CalendarContract.Instances.EVENT_ID + " = ?"
|
||||
val selArgs = arrayOf(id.toString())
|
||||
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, null)
|
||||
?: return null
|
||||
if (cursor.moveToNext()) {
|
||||
val title = cursor.getStringOrNull(1) ?: ""
|
||||
val begin = cursor.getLong(2)
|
||||
val end = cursor.getLong(3)
|
||||
val allday = cursor.getInt(4) != 0
|
||||
val color = cursor.getInt(5)
|
||||
val location = cursor.getStringOrNull(6)
|
||||
val calendar = cursor.getLong(7)
|
||||
val description = cursor.getStringOrNull(8)
|
||||
?: ""
|
||||
cursor.close()
|
||||
val proj = arrayOf(
|
||||
CalendarContract.Attendees.EVENT_ID,
|
||||
CalendarContract.Attendees.ATTENDEE_NAME,
|
||||
CalendarContract.Attendees.ATTENDEE_EMAIL
|
||||
)
|
||||
val sel = "${CalendarContract.Attendees.EVENT_ID} = $id"
|
||||
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
|
||||
val cur = context.contentResolver.query(
|
||||
CalendarContract.Attendees.CONTENT_URI,
|
||||
proj, sel, null, s
|
||||
) ?: return null
|
||||
val attendees = mutableListOf<String>()
|
||||
while (cur.moveToNext()) {
|
||||
attendees.add(
|
||||
cur.getStringOrNull(1).takeUnless { it.isNullOrBlank() }
|
||||
?: cur.getStringOrNull(2)
|
||||
?: continue
|
||||
return AndroidCalendarProvider(context).get(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class SerializedCalendarEvent(
|
||||
val id: String? = null,
|
||||
val authority: String? = null,
|
||||
val color: Int? = null,
|
||||
val startTime: Long? = null,
|
||||
val endTime: Long? = null,
|
||||
val allDay: Boolean? = null,
|
||||
val description: String? = null,
|
||||
val calendarName: String? = null,
|
||||
val location: String? = null,
|
||||
val attendees: List<String>? = null,
|
||||
val uri: String? = null,
|
||||
val completed: Boolean? = null,
|
||||
val label: String? = null,
|
||||
val timestamp: Long = 0L,
|
||||
val strategy: StorageStrategy = StorageStrategy.StoreCopy,
|
||||
)
|
||||
|
||||
class PluginCalendarEventSerializer: SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as PluginCalendarEvent
|
||||
if (searchable.storageStrategy == StorageStrategy.StoreCopy) {
|
||||
return Json.Lenient.encodeToString(
|
||||
SerializedCalendarEvent(
|
||||
id = searchable.id,
|
||||
authority = searchable.authority,
|
||||
color = searchable.color,
|
||||
startTime = searchable.startTime,
|
||||
endTime = searchable.endTime,
|
||||
allDay = searchable.allDay,
|
||||
description = searchable.description,
|
||||
calendarName = searchable.calendarName,
|
||||
location = searchable.location,
|
||||
attendees = searchable.attendees,
|
||||
uri = searchable.uri.toString(),
|
||||
completed = searchable.isCompleted,
|
||||
label = searchable.label,
|
||||
strategy = searchable.storageStrategy,
|
||||
timestamp = searchable.timestamp,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return Json.Lenient.encodeToString(
|
||||
SerializedCalendarEvent(
|
||||
id = searchable.id,
|
||||
authority = searchable.authority,
|
||||
strategy = searchable.storageStrategy,
|
||||
)
|
||||
}
|
||||
cur.close()
|
||||
val tzOffset = if (allday) {
|
||||
Calendar.getInstance().timeZone.getOffset(begin)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
return AndroidCalendarEvent(
|
||||
label = title,
|
||||
id = id,
|
||||
color = color,
|
||||
startTime = begin - tzOffset,
|
||||
endTime = end - tzOffset - if (allday) 1 else 0,
|
||||
allDay = allday,
|
||||
location = location ?: "",
|
||||
attendees = attendees,
|
||||
description = description,
|
||||
calendar = calendar
|
||||
)
|
||||
}
|
||||
cursor.close()
|
||||
return null
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = PluginCalendarEvent.Domain
|
||||
}
|
||||
|
||||
class PluginCalendarEventDeserializer(
|
||||
val context: Context,
|
||||
private val pluginRepository: PluginRepository,
|
||||
): SearchableDeserializer {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
val json = Json.Lenient.decodeFromString<SerializedCalendarEvent>(serialized)
|
||||
val authority = json.authority ?: return null
|
||||
val id = json.id ?: return null
|
||||
val strategy = json.strategy
|
||||
val plugin = pluginRepository.get(authority).firstOrNull() ?: return null
|
||||
if (!plugin.enabled) return null
|
||||
|
||||
return when(strategy) {
|
||||
StorageStrategy.StoreReference -> {
|
||||
PluginCalendarProvider(context, authority).get(id).getOrNull()
|
||||
}
|
||||
else -> {
|
||||
val timestamp = json.timestamp
|
||||
PluginCalendarEvent(
|
||||
id = id,
|
||||
color = json.color,
|
||||
startTime = json.startTime ?: 0,
|
||||
endTime = json.endTime ?: 0,
|
||||
allDay = json.allDay ?: false,
|
||||
description = json.description,
|
||||
calendarName = json.calendarName,
|
||||
location = json.location,
|
||||
attendees = json.attendees ?: emptyList(),
|
||||
label = json.label ?: return null,
|
||||
uri = Uri.parse(json.uri ?: return null),
|
||||
isCompleted = json.completed,
|
||||
storageStrategy = StorageStrategy.StoreCopy,
|
||||
authority = authority,
|
||||
timestamp = timestamp,
|
||||
updatedSelf = {
|
||||
if (it !is PluginCalendarEvent) UpdateResult.TemporarilyUnavailable()
|
||||
else PluginCalendarProvider(context, authority).refresh(it, timestamp).asUpdateResult()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
|
||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarEvent
|
||||
import de.mm20.launcher2.calendar.providers.PluginCalendarEvent
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
@ -9,6 +11,7 @@ import org.koin.dsl.module
|
||||
|
||||
val calendarModule = module {
|
||||
factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { get<CalendarRepository>() }
|
||||
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get(), get()) }
|
||||
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { CalendarEventDeserializer(androidContext()) }
|
||||
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get(), get(), get()) }
|
||||
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { AndroidCalendarEventDeserializer(androidContext()) }
|
||||
factory<SearchableDeserializer>(named(PluginCalendarEvent.Domain)) { PluginCalendarEventDeserializer(androidContext(), get()) }
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
package de.mm20.launcher2.calendar.providers
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
@ -6,6 +6,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import de.mm20.launcher2.calendar.AndroidCalendarEventSerializer
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
@ -21,7 +22,8 @@ internal data class AndroidCalendarEvent(
|
||||
override val location: String?,
|
||||
override val attendees: List<String>,
|
||||
override val description: String?,
|
||||
val calendar: Long,
|
||||
internal val calendarId: Long,
|
||||
override val calendarName: String?,
|
||||
override val labelOverride: String? = null,
|
||||
) : CalendarEvent {
|
||||
|
||||
@ -61,7 +63,7 @@ internal data class AndroidCalendarEvent(
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return CalendarEventSerializer()
|
||||
return AndroidCalendarEventSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -69,9 +71,10 @@ internal data class AndroidCalendarEvent(
|
||||
}
|
||||
}
|
||||
|
||||
data class UserCalendar(
|
||||
val id: Long,
|
||||
data class CalendarList(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val owner: String,
|
||||
val color: Int
|
||||
val owner: String?,
|
||||
val color: Int,
|
||||
val providerId: String,
|
||||
)
|
||||
@ -0,0 +1,218 @@
|
||||
package de.mm20.launcher2.calendar.providers
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.provider.CalendarContract
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Calendar
|
||||
|
||||
class AndroidCalendarProvider(
|
||||
private val context: Context,
|
||||
): CalendarProvider {
|
||||
override suspend fun search(
|
||||
query: String?,
|
||||
from: Long,
|
||||
to: Long,
|
||||
excludedCalendars: List<String>,
|
||||
excludeAllDayEvents: Boolean,
|
||||
allowNetwork: Boolean
|
||||
): List<CalendarEvent> {
|
||||
val results = withContext(Dispatchers.IO) {
|
||||
val results = mutableListOf<AndroidCalendarEvent>()
|
||||
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
|
||||
ContentUris.appendId(builder, from)
|
||||
ContentUris.appendId(builder, to)
|
||||
val uri = builder.build()
|
||||
val projection = arrayOf(
|
||||
CalendarContract.Instances.EVENT_ID,
|
||||
CalendarContract.Instances.TITLE,
|
||||
CalendarContract.Instances.BEGIN,
|
||||
CalendarContract.Instances.END,
|
||||
CalendarContract.Instances.ALL_DAY,
|
||||
CalendarContract.Instances.DISPLAY_COLOR,
|
||||
CalendarContract.Instances.EVENT_LOCATION,
|
||||
CalendarContract.Instances.CALENDAR_ID,
|
||||
CalendarContract.Instances.DESCRIPTION,
|
||||
CalendarContract.Instances.CALENDAR_DISPLAY_NAME,
|
||||
)
|
||||
val selection = mutableListOf<String>()
|
||||
if (query != null) selection.add("${CalendarContract.Instances.TITLE} LIKE ?")
|
||||
if (excludedCalendars.isNotEmpty()) selection.add("${CalendarContract.Instances.CALENDAR_ID} NOT IN (${excludedCalendars.joinToString()})")
|
||||
if (excludeAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0")
|
||||
val selArgs = if (query != null) arrayOf("%$query%") else null
|
||||
val sort = "${CalendarContract.Instances.BEGIN} ASC"
|
||||
val cursor = context.contentResolver.query(
|
||||
uri,
|
||||
projection,
|
||||
selection.joinToString(separator = " AND "),
|
||||
selArgs,
|
||||
sort
|
||||
) ?: return@withContext mutableListOf()
|
||||
val proj = arrayOf(
|
||||
CalendarContract.Attendees.EVENT_ID,
|
||||
CalendarContract.Attendees.ATTENDEE_NAME,
|
||||
CalendarContract.Attendees.ATTENDEE_EMAIL
|
||||
)
|
||||
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
|
||||
while (cursor.moveToNext()) {
|
||||
val sel = "${CalendarContract.Attendees.EVENT_ID} = ${cursor.getLong(0)}"
|
||||
val cur = context.contentResolver.query(
|
||||
CalendarContract.Attendees.CONTENT_URI,
|
||||
proj, sel, null, s
|
||||
) ?: return@withContext mutableListOf()
|
||||
val attendees = mutableListOf<String>()
|
||||
while (cur.moveToNext()) {
|
||||
attendees.add(
|
||||
cur.getStringOrNull(1).takeUnless { it.isNullOrBlank() }
|
||||
?: cur.getStringOrNull(2)
|
||||
?: continue
|
||||
)
|
||||
}
|
||||
cur.close()
|
||||
val allday = cursor.getInt(4) > 0
|
||||
val begin = cursor.getLong(2)
|
||||
|
||||
val tzOffset = if (allday) {
|
||||
Calendar.getInstance().timeZone.getOffset(begin)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val event = AndroidCalendarEvent(
|
||||
label = cursor.getStringOrNull(1) ?: "",
|
||||
id = cursor.getLong(0),
|
||||
color = cursor.getInt(5),
|
||||
startTime = begin - tzOffset,
|
||||
endTime = cursor.getLong(3) - tzOffset - if (allday) 1 else 0,
|
||||
allDay = allday,
|
||||
location = cursor.getStringOrNull(6) ?: "",
|
||||
attendees = attendees,
|
||||
description = cursor.getStringOrNull(8)
|
||||
?: "",
|
||||
calendarId = cursor.getLong(7),
|
||||
calendarName = cursor.getStringOrNull(9)
|
||||
)
|
||||
results.add(event)
|
||||
}
|
||||
cursor.close()
|
||||
return@withContext results
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
suspend fun get(id: Long): CalendarEvent? = withContext(Dispatchers.IO) {
|
||||
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
|
||||
ContentUris.appendId(builder, System.currentTimeMillis())
|
||||
ContentUris.appendId(builder, System.currentTimeMillis() + 63072000000L)
|
||||
val uri = builder.build()
|
||||
val projection = arrayOf(
|
||||
CalendarContract.Instances.EVENT_ID,
|
||||
CalendarContract.Instances.TITLE,
|
||||
CalendarContract.Instances.BEGIN,
|
||||
CalendarContract.Instances.END,
|
||||
CalendarContract.Instances.ALL_DAY,
|
||||
CalendarContract.Instances.DISPLAY_COLOR,
|
||||
CalendarContract.Instances.EVENT_LOCATION,
|
||||
CalendarContract.Instances.CALENDAR_ID,
|
||||
CalendarContract.Instances.DESCRIPTION,
|
||||
CalendarContract.Instances.CALENDAR_DISPLAY_NAME,
|
||||
)
|
||||
val selection = CalendarContract.Instances.EVENT_ID + " = ?"
|
||||
val selArgs = arrayOf(id.toString())
|
||||
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, null)
|
||||
?: return@withContext null
|
||||
if (cursor.moveToNext()) {
|
||||
val title = cursor.getStringOrNull(1) ?: ""
|
||||
val begin = cursor.getLong(2)
|
||||
val end = cursor.getLong(3)
|
||||
val allday = cursor.getInt(4) != 0
|
||||
val color = cursor.getInt(5)
|
||||
val location = cursor.getStringOrNull(6)
|
||||
val calendar = cursor.getLong(7)
|
||||
val description = cursor.getStringOrNull(8)
|
||||
val calendarName = cursor.getStringOrNull(9)
|
||||
cursor.close()
|
||||
val proj = arrayOf(
|
||||
CalendarContract.Attendees.EVENT_ID,
|
||||
CalendarContract.Attendees.ATTENDEE_NAME,
|
||||
CalendarContract.Attendees.ATTENDEE_EMAIL
|
||||
)
|
||||
val sel = "${CalendarContract.Attendees.EVENT_ID} = $id"
|
||||
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
|
||||
val cur = context.contentResolver.query(
|
||||
CalendarContract.Attendees.CONTENT_URI,
|
||||
proj, sel, null, s
|
||||
) ?: return@withContext null
|
||||
val attendees = mutableListOf<String>()
|
||||
while (cur.moveToNext()) {
|
||||
attendees.add(
|
||||
cur.getStringOrNull(1).takeUnless { it.isNullOrBlank() }
|
||||
?: cur.getStringOrNull(2)
|
||||
?: continue
|
||||
)
|
||||
}
|
||||
cur.close()
|
||||
val tzOffset = if (allday) {
|
||||
Calendar.getInstance().timeZone.getOffset(begin)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
return@withContext AndroidCalendarEvent(
|
||||
label = title,
|
||||
id = id,
|
||||
color = color,
|
||||
startTime = begin - tzOffset,
|
||||
endTime = end - tzOffset - if (allday) 1 else 0,
|
||||
allDay = allday,
|
||||
location = location ?: "",
|
||||
attendees = attendees,
|
||||
description = description,
|
||||
calendarId = calendar,
|
||||
calendarName = calendarName
|
||||
)
|
||||
}
|
||||
cursor.close()
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
override suspend fun getCalendarLists(): List<CalendarList> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val calendars = mutableListOf<CalendarList>()
|
||||
val uri = CalendarContract.Calendars.CONTENT_URI
|
||||
val proj = arrayOf(
|
||||
CalendarContract.Calendars._ID,
|
||||
CalendarContract.Calendars.NAME,
|
||||
CalendarContract.Calendars.ACCOUNT_NAME,
|
||||
CalendarContract.Calendars.CALENDAR_COLOR,
|
||||
CalendarContract.Calendars.VISIBLE,
|
||||
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
|
||||
)
|
||||
val cursor = context.contentResolver.query(uri, proj, null, null, null)
|
||||
?: return@withContext emptyList()
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
calendars.add(
|
||||
CalendarList(
|
||||
id = "local:${cursor.getLong(0)}",
|
||||
name = cursor.getStringOrNull(5) ?: cursor.getStringOrNull(1) ?: "",
|
||||
owner = cursor.getStringOrNull(2),
|
||||
color = cursor.getInt(3),
|
||||
providerId = "local",
|
||||
)
|
||||
)
|
||||
} catch (e: NullPointerException) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
cursor.close()
|
||||
calendars.sortBy { it.owner }
|
||||
return@withContext calendars
|
||||
}
|
||||
}
|
||||
|
||||
override val namespace: String = "local"
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package de.mm20.launcher2.calendar.providers
|
||||
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
|
||||
internal interface CalendarProvider {
|
||||
suspend fun search(
|
||||
query: String? = null,
|
||||
from: Long = System.currentTimeMillis(),
|
||||
to: Long = from + 14 * 24 * 60 * 60 * 1000L,
|
||||
excludedCalendars: List<String> = emptyList(),
|
||||
excludeAllDayEvents: Boolean = false,
|
||||
allowNetwork: Boolean = false,
|
||||
): List<CalendarEvent>
|
||||
|
||||
suspend fun getCalendarLists(): List<CalendarList>
|
||||
|
||||
val namespace: String
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package de.mm20.launcher2.calendar.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.calendar.PluginCalendarEventSerializer
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.File
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.UpdatableSearchable
|
||||
import de.mm20.launcher2.search.UpdateResult
|
||||
|
||||
data class PluginCalendarEvent(
|
||||
val id: String,
|
||||
val authority: String,
|
||||
override val color: Int?,
|
||||
override val startTime: Long?,
|
||||
override val endTime: Long,
|
||||
override val allDay: Boolean,
|
||||
override val description: String?,
|
||||
override val calendarName: String?,
|
||||
override val location: String?,
|
||||
override val attendees: List<String>,
|
||||
val uri: Uri,
|
||||
override val isCompleted: Boolean?,
|
||||
override val label: String,
|
||||
override val labelOverride: String? = null,
|
||||
override val timestamp: Long,
|
||||
internal val storageStrategy: StorageStrategy,
|
||||
override val updatedSelf: (suspend (SavableSearchable) -> UpdateResult<CalendarEvent>)?,
|
||||
) : CalendarEvent, UpdatableSearchable<CalendarEvent> {
|
||||
override val domain: String = Domain
|
||||
|
||||
override val key: String
|
||||
get() = "$domain://$authority:$id"
|
||||
|
||||
override fun overrideLabel(label: String): SavableSearchable {
|
||||
return copy(
|
||||
labelOverride = label
|
||||
)
|
||||
}
|
||||
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
return context.tryStartActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW
|
||||
).apply {
|
||||
data = uri
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}, options
|
||||
)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return PluginCalendarEventSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val Domain = "plugin.calendar"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,186 @@
|
||||
package de.mm20.launcher2.calendar.providers
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.CancellationSignal
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.plugin.QueryPluginApi
|
||||
import de.mm20.launcher2.plugin.contracts.CalendarPluginContract
|
||||
import de.mm20.launcher2.plugin.contracts.CalendarPluginContract.CalendarListColumns
|
||||
import de.mm20.launcher2.plugin.contracts.CalendarPluginContract.EventColumns
|
||||
import de.mm20.launcher2.plugin.data.set
|
||||
import de.mm20.launcher2.plugin.data.withColumns
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.UpdateResult
|
||||
import de.mm20.launcher2.search.asUpdateResult
|
||||
import de.mm20.launcher2.search.calendar.CalendarQuery
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class PluginCalendarProvider(
|
||||
private val context: Context,
|
||||
private val pluginAuthority: String,
|
||||
) : QueryPluginApi<CalendarQuery, PluginCalendarEvent>(
|
||||
context, pluginAuthority
|
||||
), CalendarProvider {
|
||||
override suspend fun search(
|
||||
query: String?,
|
||||
from: Long,
|
||||
to: Long,
|
||||
excludedCalendars: List<String>,
|
||||
excludeAllDayEvents: Boolean,
|
||||
allowNetwork: Boolean
|
||||
): List<CalendarEvent> {
|
||||
return search(
|
||||
CalendarQuery(
|
||||
query = query,
|
||||
start = from,
|
||||
end = to,
|
||||
excludedCalendars = excludedCalendars,
|
||||
),
|
||||
allowNetwork,
|
||||
)
|
||||
}
|
||||
|
||||
override fun Uri.Builder.appendQueryParameters(query: CalendarQuery): Uri.Builder {
|
||||
appendQueryParameter(
|
||||
CalendarPluginContract.Params.Query,
|
||||
query.query
|
||||
)
|
||||
val start = query.start
|
||||
if (start != null) {
|
||||
appendQueryParameter(
|
||||
CalendarPluginContract.Params.StartTime,
|
||||
start.toString()
|
||||
)
|
||||
}
|
||||
val end = query.end
|
||||
if (end != null) {
|
||||
appendQueryParameter(
|
||||
CalendarPluginContract.Params.EndTime,
|
||||
end.toString()
|
||||
)
|
||||
}
|
||||
if (query.excludedCalendars.isNotEmpty()) {
|
||||
appendQueryParameter(
|
||||
CalendarPluginContract.Params.Exclude,
|
||||
query.excludedCalendars.joinToString(",")
|
||||
)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun Cursor.getData(): List<PluginCalendarEvent>? {
|
||||
val config = getConfig()
|
||||
val cursor = this
|
||||
|
||||
if (config == null) {
|
||||
Log.e("MM20", "Plugin ${pluginAuthority} returned null config")
|
||||
cursor.close()
|
||||
return null
|
||||
}
|
||||
|
||||
val results = mutableListOf<PluginCalendarEvent>()
|
||||
val timestamp = System.currentTimeMillis()
|
||||
cursor.withColumns(EventColumns) {
|
||||
while (cursor.moveToNext()) {
|
||||
results += PluginCalendarEvent(
|
||||
id = cursor[EventColumns.Id] ?: continue,
|
||||
authority = pluginAuthority,
|
||||
uri = Uri.parse(cursor[EventColumns.Uri] ?: continue),
|
||||
color = cursor[EventColumns.Color],
|
||||
label = cursor[EventColumns.Title] ?: continue,
|
||||
description = cursor[EventColumns.Description],
|
||||
location = cursor[EventColumns.Location],
|
||||
calendarName = cursor[EventColumns.CalendarName],
|
||||
allDay = cursor[EventColumns.IncludeTime] == false,
|
||||
startTime = cursor[EventColumns.StartTime],
|
||||
endTime = cursor[EventColumns.EndTime] ?: continue,
|
||||
attendees = cursor[EventColumns.Attendees] ?: emptyList(),
|
||||
isCompleted = cursor[EventColumns.IsCompleted],
|
||||
updatedSelf = {
|
||||
if (it !is PluginCalendarEvent) UpdateResult.TemporarilyUnavailable()
|
||||
else refresh(it, timestamp).asUpdateResult()
|
||||
},
|
||||
storageStrategy = config.storageStrategy,
|
||||
timestamp = timestamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
cursor.close()
|
||||
return results
|
||||
}
|
||||
|
||||
override fun PluginCalendarEvent.toBundle(): Bundle {
|
||||
return Bundle().apply {
|
||||
set(EventColumns.Id, id)
|
||||
set(EventColumns.Title, label)
|
||||
set(EventColumns.Description, description)
|
||||
set(EventColumns.CalendarName, calendarName)
|
||||
set(EventColumns.Location, location)
|
||||
set(EventColumns.Color, color)
|
||||
set(EventColumns.StartTime, startTime)
|
||||
set(EventColumns.EndTime, endTime)
|
||||
set(EventColumns.IncludeTime, !allDay)
|
||||
set(EventColumns.Attendees, attendees)
|
||||
set(EventColumns.Uri, uri.toString())
|
||||
set(EventColumns.IsCompleted, isCompleted)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getCalendarLists(): List<CalendarList> = withContext(Dispatchers.IO) {
|
||||
val uri = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(pluginAuthority)
|
||||
.path(CalendarPluginContract.Paths.CalendarLists)
|
||||
.build()
|
||||
val cancellationSignal = CancellationSignal()
|
||||
|
||||
return@withContext suspendCancellableCoroutine {
|
||||
it.invokeOnCancellation {
|
||||
cancellationSignal.cancel()
|
||||
}
|
||||
val cursor = try {
|
||||
context.contentResolver.query(
|
||||
uri,
|
||||
null,
|
||||
null,
|
||||
cancellationSignal
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MM20", "Plugin $pluginAuthority threw exception", e)
|
||||
it.resume(emptyList())
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
if (cursor == null) {
|
||||
Log.e("MM20", "Plugin $pluginAuthority returned null cursor")
|
||||
it.resume(emptyList())
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
val results = mutableListOf<CalendarList>()
|
||||
cursor.use {
|
||||
cursor.withColumns(CalendarListColumns) {
|
||||
while (cursor.moveToNext()) {
|
||||
results += CalendarList(
|
||||
id = "${pluginAuthority}:" + (cursor[CalendarListColumns.Id] ?: continue),
|
||||
color = cursor[CalendarListColumns.Color] ?: 0,
|
||||
name = cursor[CalendarListColumns.Name] ?: continue,
|
||||
owner = cursor[CalendarListColumns.AccountName],
|
||||
providerId = pluginAuthority,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it.resume(results)
|
||||
}
|
||||
}
|
||||
|
||||
override val namespace: String = pluginAuthority
|
||||
}
|
||||
@ -95,6 +95,7 @@ internal class PluginLocationProvider(
|
||||
)
|
||||
}
|
||||
}
|
||||
cursor.close()
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
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.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -15,7 +10,11 @@ import java.util.UUID
|
||||
@Serializable
|
||||
data class CalendarWidgetConfig(
|
||||
val allDayEvents: Boolean = true,
|
||||
val excludedCalendarIds: List<Long> = emptyList(),
|
||||
@Deprecated("Use excludedCalendars instead")
|
||||
@SerialName("excludedCalendarIds")
|
||||
val legacyExcludedCalendarIds: List<Long>? = null,
|
||||
@SerialName("excludedCalendars")
|
||||
val excludedCalendarIds: List<String>? = null,
|
||||
)
|
||||
data class CalendarWidget(
|
||||
override val id: UUID,
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
package de.mm20.launcher2.sdk.calendar
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
data class CalendarEvent(
|
||||
val id: String,
|
||||
val title: String,
|
||||
/**
|
||||
* The name of the calendar the event belongs to.
|
||||
*/
|
||||
val calendarName: String?,
|
||||
val description: String? = null,
|
||||
/**
|
||||
* The location of the event.
|
||||
*/
|
||||
val location: String? = null,
|
||||
/**
|
||||
* The color of the event, as 0xAARRGGBB int.
|
||||
*/
|
||||
val color: Int? = null,
|
||||
/**
|
||||
* Start time of the event in milliseconds since epoch.
|
||||
* For tasks, this can be null.
|
||||
*/
|
||||
val startTime: Long?,
|
||||
/**
|
||||
* End time of the event in milliseconds since epoch.
|
||||
* For tasks: Due date of the task.
|
||||
*/
|
||||
val endTime: Long,
|
||||
/**
|
||||
* If false, only the date will be shown for the event.
|
||||
*/
|
||||
val includeTime: Boolean = true,
|
||||
val attendees: List<String> = emptyList(),
|
||||
val uri: Uri,
|
||||
/**
|
||||
* If this is not null, the event is treated as a task, indicated by a checkmark in the UI.
|
||||
*/
|
||||
val isCompleted: Boolean? = null,
|
||||
)
|
||||
@ -0,0 +1,8 @@
|
||||
package de.mm20.launcher2.sdk.calendar
|
||||
|
||||
data class CalendarList(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val accountName: String? = null,
|
||||
val color: Int? = null,
|
||||
)
|
||||
@ -0,0 +1,110 @@
|
||||
package de.mm20.launcher2.sdk.calendar
|
||||
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.CancellationSignal
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
import de.mm20.launcher2.plugin.config.QueryPluginConfig
|
||||
import de.mm20.launcher2.plugin.contracts.CalendarPluginContract
|
||||
import de.mm20.launcher2.plugin.contracts.CalendarPluginContract.CalendarListColumns
|
||||
import de.mm20.launcher2.plugin.contracts.CalendarPluginContract.EventColumns
|
||||
import de.mm20.launcher2.plugin.contracts.SearchPluginContract
|
||||
import de.mm20.launcher2.plugin.data.buildCursor
|
||||
import de.mm20.launcher2.plugin.data.get
|
||||
import de.mm20.launcher2.sdk.base.QueryPluginProvider
|
||||
import de.mm20.launcher2.sdk.utils.launchWithCancellationSignal
|
||||
import de.mm20.launcher2.search.calendar.CalendarQuery
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
abstract class CalendarProvider(
|
||||
config: QueryPluginConfig,
|
||||
) : QueryPluginProvider<CalendarQuery, CalendarEvent>(
|
||||
config
|
||||
) {
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<out String>?,
|
||||
queryArgs: Bundle?,
|
||||
cancellationSignal: CancellationSignal?
|
||||
): Cursor? {
|
||||
super.query(uri, projection, queryArgs, cancellationSignal)?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
if (uri.pathSegments.size == 1 && uri.pathSegments.first() == CalendarPluginContract.Paths.CalendarLists) {
|
||||
return getCalendarLists(cancellationSignal).toCursor()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getCalendarLists(cancellationSignal: CancellationSignal?): List<CalendarList> {
|
||||
return launchWithCancellationSignal(cancellationSignal) {
|
||||
getCalendarLists()
|
||||
}
|
||||
}
|
||||
|
||||
abstract suspend fun getCalendarLists(): List<CalendarList>
|
||||
|
||||
override fun getQuery(uri: Uri): CalendarQuery {
|
||||
val query = uri.getQueryParameter(CalendarPluginContract.Params.Query)
|
||||
val start = uri.getQueryParameter(CalendarPluginContract.Params.StartTime)?.toLongOrNull()
|
||||
val end = uri.getQueryParameter(CalendarPluginContract.Params.EndTime)?.toLongOrNull()
|
||||
val excludedCalendars = uri.getQueryParameter(CalendarPluginContract.Params.Exclude)?.split(",") ?: emptyList()
|
||||
|
||||
return CalendarQuery(
|
||||
query = query,
|
||||
start = start,
|
||||
end = end,
|
||||
excludedCalendars = excludedCalendars,
|
||||
)
|
||||
}
|
||||
|
||||
override fun Bundle.toResult(): CalendarEvent? {
|
||||
return CalendarEvent(
|
||||
id = get(EventColumns.Id) ?: return null,
|
||||
title = get(EventColumns.Title) ?: return null,
|
||||
description = get(EventColumns.Description),
|
||||
location = get(EventColumns.Location),
|
||||
color = get(EventColumns.Color),
|
||||
calendarName = get(EventColumns.CalendarName),
|
||||
startTime = get(EventColumns.StartTime),
|
||||
endTime = get(EventColumns.EndTime) ?: return null,
|
||||
includeTime = get(EventColumns.IncludeTime) ?: true,
|
||||
attendees = get(EventColumns.Attendees) ?: emptyList(),
|
||||
uri = Uri.parse(get(EventColumns.Uri) ?: return null),
|
||||
)
|
||||
}
|
||||
|
||||
override fun List<CalendarEvent>.toCursor(): Cursor {
|
||||
return buildCursor(EventColumns, this) {
|
||||
put(EventColumns.Id, it.id)
|
||||
put(EventColumns.Title, it.title)
|
||||
put(EventColumns.Description, it.description)
|
||||
put(EventColumns.Location, it.location)
|
||||
put(EventColumns.CalendarName, it.calendarName)
|
||||
put(EventColumns.Color, it.color)
|
||||
put(EventColumns.StartTime, it.startTime)
|
||||
put(EventColumns.EndTime, it.endTime)
|
||||
put(EventColumns.IncludeTime, it.includeTime)
|
||||
put(EventColumns.Attendees, it.attendees)
|
||||
put(EventColumns.Uri, it.uri.toString())
|
||||
put(EventColumns.IsCompleted, it.isCompleted)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPluginType(): PluginType {
|
||||
return PluginType.Calendar
|
||||
}
|
||||
|
||||
private fun List<CalendarList>.toCursor(): Cursor {
|
||||
return buildCursor(CalendarListColumns, this) {
|
||||
put(CalendarListColumns.Id, it.id)
|
||||
put(CalendarListColumns.Name, it.name)
|
||||
put(CalendarListColumns.Color, it.color)
|
||||
put(CalendarListColumns.AccountName, it.accountName)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user