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">
|
<component name="InspectionProjectProfileManager">
|
||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<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">
|
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
|
|||||||
@ -51,14 +51,14 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
|
|||||||
val frequentlyUsedRows = it.second.frequentlyUsedRows
|
val frequentlyUsedRows = it.second.frequentlyUsedRows
|
||||||
|
|
||||||
val pinned = favoritesService.getFavorites(
|
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,
|
minPinnedLevel = PinnedLevel.AutomaticallySorted,
|
||||||
limit = 10 * columns,
|
limit = 10 * columns,
|
||||||
)
|
)
|
||||||
if (includeFrequentlyUsed) {
|
if (includeFrequentlyUsed) {
|
||||||
emitAll(pinned.flatMapLatest { pinned ->
|
emitAll(pinned.flatMapLatest { pinned ->
|
||||||
favoritesService.getFavorites(
|
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,
|
maxPinnedLevel = PinnedLevel.FrequentlyUsed,
|
||||||
minPinnedLevel = PinnedLevel.FrequentlyUsed,
|
minPinnedLevel = PinnedLevel.FrequentlyUsed,
|
||||||
limit = frequentlyUsedRows * columns - pinned.size % columns,
|
limit = frequentlyUsedRows * columns - pinned.size % columns,
|
||||||
|
|||||||
@ -409,15 +409,15 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
val missingCalendarPermission = combine(
|
val missingCalendarPermission = combine(
|
||||||
permissionsManager.hasPermission(PermissionGroup.Calendar),
|
permissionsManager.hasPermission(PermissionGroup.Calendar),
|
||||||
calendarSearchSettings.enabled,
|
calendarSearchSettings.providers,
|
||||||
) { perm, enabled -> !perm && enabled }
|
) { perm, providers -> !perm && providers.contains("local") }
|
||||||
|
|
||||||
fun requestCalendarPermission(context: AppCompatActivity) {
|
fun requestCalendarPermission(context: AppCompatActivity) {
|
||||||
permissionsManager.requestPermission(context, PermissionGroup.Calendar)
|
permissionsManager.requestPermission(context, PermissionGroup.Calendar)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disableCalendarSearch() {
|
fun disableCalendarSearch() {
|
||||||
calendarSearchSettings.setEnabled(false)
|
calendarSearchSettings.setProviderEnabled("local", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val missingContactsPermission = combine(
|
val missingContactsPermission = combine(
|
||||||
|
|||||||
@ -8,10 +8,10 @@ import androidx.compose.animation.SharedTransitionLayout
|
|||||||
import androidx.compose.animation.core.MutableTransitionState
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.expandIn
|
import androidx.compose.animation.expandIn
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkOut
|
import androidx.compose.animation.shrinkOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
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.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
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.Notes
|
||||||
import androidx.compose.material.icons.rounded.OpenInNew
|
import androidx.compose.material.icons.rounded.OpenInNew
|
||||||
import androidx.compose.material.icons.rounded.People
|
import androidx.compose.material.icons.rounded.People
|
||||||
import androidx.compose.material.icons.rounded.Place
|
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.Schedule
|
||||||
import androidx.compose.material.icons.rounded.Star
|
import androidx.compose.material.icons.rounded.Star
|
||||||
import androidx.compose.material.icons.rounded.StarOutline
|
import androidx.compose.material.icons.rounded.StarOutline
|
||||||
import androidx.compose.material.icons.rounded.Tune
|
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.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarResult
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.roundToIntRect
|
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.search.CalendarEvent
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.DefaultToolbarAction
|
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.LocalDarkTheme
|
||||||
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.ui.locals.LocalSnackbarHostState
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import palettes.TonalPalette
|
import palettes.TonalPalette
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -99,33 +93,53 @@ fun CalendarItem(
|
|||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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
|
modifier = Modifier
|
||||||
.padding(horizontal = 14.dp)
|
.padding(horizontal = 14.dp, vertical = 20.dp)
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.sharedBounds(
|
.sharedElement(
|
||||||
rememberSharedContentState("color"),
|
rememberSharedContentState("color"),
|
||||||
this@AnimatedContent
|
this@AnimatedContent
|
||||||
)
|
),
|
||||||
.background(eventColor, MaterialTheme.shapes.extraSmall)
|
tint = eventColor
|
||||||
)
|
)
|
||||||
|
Column(
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 4.dp, end = 16.dp)
|
.padding(start = 4.dp, end = 16.dp)
|
||||||
.sharedBounds(
|
) {
|
||||||
rememberSharedContentState("label"),
|
Text(
|
||||||
this@AnimatedContent
|
modifier = Modifier
|
||||||
),
|
.sharedBounds(
|
||||||
text = calendar.labelOverride ?: calendar.label,
|
rememberSharedContentState("label"),
|
||||||
style = MaterialTheme.typography.titleMedium
|
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(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth().padding(bottom = 8.dp, end = 16.dp),
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 8.dp, end = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -146,7 +160,8 @@ fun CalendarItem(
|
|||||||
if (!calendar.description.isNullOrBlank()) {
|
if (!calendar.description.isNullOrBlank()) {
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth().padding(bottom = 8.dp, end = 16.dp),
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 8.dp, end = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -165,7 +180,8 @@ fun CalendarItem(
|
|||||||
if (calendar.attendees.isNotEmpty()) {
|
if (calendar.attendees.isNotEmpty()) {
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth().padding(bottom = 8.dp, end = 16.dp),
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 8.dp, end = 16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -261,15 +277,21 @@ fun CalendarItem(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Box(
|
Icon(
|
||||||
|
when (calendar.isCompleted) {
|
||||||
|
true -> Icons.Rounded.CheckCircle
|
||||||
|
false -> Icons.Rounded.RadioButtonUnchecked
|
||||||
|
null -> Icons.Rounded.Circle
|
||||||
|
},
|
||||||
|
null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 16.dp)
|
.padding(end = 16.dp)
|
||||||
.size(20.dp)
|
.size(20.dp)
|
||||||
.sharedBounds(
|
.sharedElement(
|
||||||
rememberSharedContentState("color"),
|
rememberSharedContentState("color"),
|
||||||
this@AnimatedContent
|
this@AnimatedContent
|
||||||
)
|
),
|
||||||
.background(eventColor, MaterialTheme.shapes.extraSmall)
|
tint = eventColor
|
||||||
)
|
)
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -281,13 +303,16 @@ fun CalendarItem(
|
|||||||
style = MaterialTheme.typography.titleSmall
|
style = MaterialTheme.typography.titleSmall
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(top = 2.dp)
|
modifier = Modifier
|
||||||
|
.padding(top = 2.dp)
|
||||||
.sharedBounds(
|
.sharedBounds(
|
||||||
rememberSharedContentState("date"),
|
rememberSharedContentState("date"),
|
||||||
this@AnimatedContent
|
this@AnimatedContent
|
||||||
),
|
),
|
||||||
text = calendar.getSummary(context),
|
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 {
|
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(
|
if (allDay) return DateUtils.formatDateRange(
|
||||||
context,
|
context,
|
||||||
startTime,
|
startTime,
|
||||||
@ -343,6 +386,39 @@ private fun CalendarEvent.formatTime(context: Context): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun CalendarEvent.getSummary(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 =
|
val isToday =
|
||||||
DateUtils.isToday(startTime) && DateUtils.isToday(endTime)
|
DateUtils.isToday(startTime) && DateUtils.isToday(endTime)
|
||||||
return if (isToday) {
|
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.LinkOff
|
||||||
import androidx.compose.material.icons.rounded.OpenInNew
|
import androidx.compose.material.icons.rounded.OpenInNew
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -64,7 +63,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.isUnspecified
|
import androidx.compose.ui.unit.isUnspecified
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import de.mm20.launcher2.calendar.CalendarRepository
|
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.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
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.DragResizeHandle
|
||||||
import de.mm20.launcher2.ui.component.LargeMessage
|
import de.mm20.launcher2.ui.component.LargeMessage
|
||||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
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.CheckboxPreference
|
||||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||||
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
|
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.NotesWidget
|
||||||
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 kotlinx.coroutines.flow.map
|
||||||
import org.koin.androidx.compose.get
|
import org.koin.androidx.compose.get
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
@ -504,18 +503,16 @@ fun ColumnScope.ConfigureCalendarWidget(
|
|||||||
) {
|
) {
|
||||||
val calendarRepository: CalendarRepository = get()
|
val calendarRepository: CalendarRepository = get()
|
||||||
val permissionsManager: PermissionsManager = get()
|
val permissionsManager: PermissionsManager = get()
|
||||||
var calendars by remember { mutableStateOf(emptyList<UserCalendar>()) }
|
val calendars by remember {
|
||||||
var ready by remember { mutableStateOf(false) }
|
calendarRepository.getCalendars().map {
|
||||||
|
it.sortedBy { it.name }
|
||||||
|
}
|
||||||
|
}.collectAsState(null)
|
||||||
|
|
||||||
val hasPermission by remember {
|
val hasPermission by remember {
|
||||||
permissionsManager.hasPermission(PermissionGroup.Calendar)
|
permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||||
}.collectAsState(true)
|
}.collectAsState(true)
|
||||||
|
|
||||||
LaunchedEffect(hasPermission) {
|
|
||||||
calendars = calendarRepository.getCalendars().sortedBy { it.name }
|
|
||||||
ready = true
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedCard {
|
OutlinedCard {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
@ -537,26 +534,29 @@ fun ColumnScope.ConfigureCalendarWidget(
|
|||||||
text = stringResource(R.string.preference_calendar_calendars)
|
text = stringResource(R.string.preference_calendar_calendars)
|
||||||
)
|
)
|
||||||
val context = LocalLifecycleOwner.current as AppCompatActivity
|
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 {
|
OutlinedCard {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
for ((i, calendar) in calendars.withIndex()) {
|
for ((i, calendar) in calendars!!.withIndex()) {
|
||||||
if (i > 0) Divider()
|
if (i > 0) HorizontalDivider()
|
||||||
CheckboxPreference(
|
CheckboxPreference(
|
||||||
title = calendar.name,
|
title = calendar.name,
|
||||||
summary = calendar.owner,
|
summary = calendar.owner,
|
||||||
iconPadding = false,
|
iconPadding = false,
|
||||||
value = !widget.config.excludedCalendarIds.contains(calendar.id),
|
value = excludedCalendars.contains(calendar.id) != true,
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
onWidgetUpdated(
|
onWidgetUpdated(
|
||||||
widget.copy(
|
widget.copy(
|
||||||
config = widget.config.copy(
|
config = widget.config.copy(
|
||||||
excludedCalendarIds = if (it) {
|
excludedCalendarIds = if (it) {
|
||||||
widget.config.excludedCalendarIds - calendar.id
|
excludedCalendars - calendar.id
|
||||||
} else {
|
} 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),
|
text = stringResource(R.string.missing_permission_calendar_widget_settings),
|
||||||
onClick = { permissionsManager.requestPermission(context, PermissionGroup.Calendar) },
|
onClick = { permissionsManager.requestPermission(context, PermissionGroup.Calendar) },
|
||||||
)
|
)
|
||||||
} else if (ready) {
|
} else if (calendars != null) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
|
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
|||||||
@ -43,7 +43,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
|||||||
val calendarEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
val calendarEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
||||||
val pinnedCalendarEvents =
|
val pinnedCalendarEvents =
|
||||||
favoritesService.getFavorites(
|
favoritesService.getFavorites(
|
||||||
includeTypes = listOf("calendar"),
|
includeTypes = listOf("calendar", "plugin.calendar"),
|
||||||
minPinnedLevel = PinnedLevel.AutomaticallySorted,
|
minPinnedLevel = PinnedLevel.AutomaticallySorted,
|
||||||
).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||||
val nextEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
val nextEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
|
||||||
@ -67,10 +67,10 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
|||||||
field = value
|
field = value
|
||||||
val dates = value.flatMap {
|
val dates = value.flatMap {
|
||||||
val startDate =
|
val startDate =
|
||||||
Instant.ofEpochMilli(it.startTime).atZone(ZoneId.systemDefault()).toLocalDate()
|
it.startTime?.let { Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate() }
|
||||||
val endDate =
|
val endDate =
|
||||||
Instant.ofEpochMilli(it.endTime).atZone(ZoneId.systemDefault()).toLocalDate()
|
Instant.ofEpochMilli(it.endTime).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||||
return@flatMap listOf(
|
return@flatMap listOfNotNull(
|
||||||
startDate,
|
startDate,
|
||||||
endDate
|
endDate
|
||||||
)
|
)
|
||||||
@ -136,14 +136,14 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
|||||||
val dayStart = max(now, date.atStartOfDay().toEpochSecond(offset) * 1000)
|
val dayStart = max(now, date.atStartOfDay().toEpochSecond(offset) * 1000)
|
||||||
val dayEnd = date.plusDays(1).atStartOfDay().toEpochSecond(offset) * 1000
|
val dayEnd = date.plusDays(1).atStartOfDay().toEpochSecond(offset) * 1000
|
||||||
var events = upcomingEvents.filter {
|
var events = upcomingEvents.filter {
|
||||||
it.endTime >= dayStart && it.startTime < dayEnd
|
it.endTime >= dayStart && (it.startTime ?: it.endTime) < dayEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showRunningPastDayEvents) {
|
if (!showRunningPastDayEvents) {
|
||||||
val totalCount = events.size
|
val totalCount = events.size
|
||||||
|
|
||||||
events = events.filter {
|
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
|
it.endTime < date.atStartOfDay().plusDays(1).toEpochSecond(offset) * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,14 +169,14 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
|
|||||||
from = System.currentTimeMillis(),
|
from = System.currentTimeMillis(),
|
||||||
to = System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000L,
|
to = System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000L,
|
||||||
excludeAllDayEvents = !config.allDayEvents,
|
excludeAllDayEvents = !config.allDayEvents,
|
||||||
excludeCalendars = config.excludedCalendarIds,
|
excludeCalendars = config.excludedCalendarIds ?: config.legacyExcludedCalendarIds?.map { "local:$it" } ?: emptyList(),
|
||||||
).collectLatest { events ->
|
).collectLatest { events ->
|
||||||
searchableRepository.getKeys(
|
searchableRepository.getKeys(
|
||||||
includeTypes = listOf("calendar"),
|
includeTypes = listOf("calendar", "plugin.calendar"),
|
||||||
maxVisibility = VisibilityLevel.SearchOnly,
|
maxVisibility = VisibilityLevel.SearchOnly,
|
||||||
limit = 9999,
|
limit = 9999,
|
||||||
).collectLatest { hidden ->
|
).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.appearance.AppearanceSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen
|
import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
|
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.cards.CardsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen
|
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.colorscheme.ThemesSettingsScreen
|
import de.mm20.launcher2.ui.settings.colorscheme.ThemesSettingsScreen
|
||||||
@ -180,6 +181,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/search/files") {
|
composable("settings/search/files") {
|
||||||
FileSearchSettingsScreen()
|
FileSearchSettingsScreen()
|
||||||
}
|
}
|
||||||
|
composable("settings/search/calendar") {
|
||||||
|
CalendarSearchSettingsScreen()
|
||||||
|
}
|
||||||
composable("settings/search/searchactions") {
|
composable("settings/search/searchactions") {
|
||||||
SearchActionsSettingsScreen()
|
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.accounts.AccountType
|
||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
|
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
|
||||||
import de.mm20.launcher2.plugin.PluginState
|
import de.mm20.launcher2.plugin.PluginState
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.Banner
|
import de.mm20.launcher2.ui.component.Banner
|
||||||
@ -196,7 +197,7 @@ fun FileSearchSettingsScreen() {
|
|||||||
primaryAction = {
|
primaryAction = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
try {
|
try {
|
||||||
state.setupActivity.send()
|
state.setupActivity.sendWithBackgroundPermission(context)
|
||||||
} catch (e: PendingIntent.CanceledException) {
|
} catch (e: PendingIntent.CanceledException) {
|
||||||
CrashReporter.logException(e)
|
CrashReporter.logException(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
|
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
|
||||||
import de.mm20.launcher2.plugin.PluginState
|
import de.mm20.launcher2.plugin.PluginState
|
||||||
import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
@ -41,6 +42,7 @@ fun LocationsSettingsScreen() {
|
|||||||
val viewModel: LocationsSettingsScreenVM = viewModel()
|
val viewModel: LocationsSettingsScreenVM = viewModel()
|
||||||
|
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val osmLocations by viewModel.osmLocations.collectAsState()
|
val osmLocations by viewModel.osmLocations.collectAsState()
|
||||||
val imperialUnits by viewModel.imperialUnits.collectAsState()
|
val imperialUnits by viewModel.imperialUnits.collectAsState()
|
||||||
@ -85,7 +87,7 @@ fun LocationsSettingsScreen() {
|
|||||||
primaryAction = {
|
primaryAction = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
try {
|
try {
|
||||||
state.setupActivity.send()
|
state.setupActivity.sendWithBackgroundPermission(context)
|
||||||
} catch (e: PendingIntent.CanceledException) {
|
} catch (e: PendingIntent.CanceledException) {
|
||||||
CrashReporter.logException(e)
|
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.LightMode
|
||||||
import androidx.compose.material.icons.rounded.Place
|
import androidx.compose.material.icons.rounded.Place
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
|
import androidx.compose.material.icons.rounded.Today
|
||||||
import androidx.compose.material.icons.rounded.Verified
|
import androidx.compose.material.icons.rounded.Verified
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@ -243,6 +244,7 @@ fun PluginSettingsScreen(pluginId: String) {
|
|||||||
PluginType.FileSearch -> Icons.AutoMirrored.Rounded.InsertDriveFile
|
PluginType.FileSearch -> Icons.AutoMirrored.Rounded.InsertDriveFile
|
||||||
PluginType.Weather -> Icons.Rounded.LightMode
|
PluginType.Weather -> Icons.Rounded.LightMode
|
||||||
PluginType.LocationSearch -> Icons.Rounded.Place
|
PluginType.LocationSearch -> Icons.Rounded.Place
|
||||||
|
PluginType.Calendar -> Icons.Rounded.Today
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
@ -253,6 +255,7 @@ fun PluginSettingsScreen(pluginId: String) {
|
|||||||
PluginType.FileSearch -> stringResource(R.string.plugin_type_filesearch)
|
PluginType.FileSearch -> stringResource(R.string.plugin_type_filesearch)
|
||||||
PluginType.Weather -> stringResource(R.string.plugin_type_weather)
|
PluginType.Weather -> stringResource(R.string.plugin_type_weather)
|
||||||
PluginType.LocationSearch -> stringResource(R.string.plugin_type_locationsearch)
|
PluginType.LocationSearch -> stringResource(R.string.plugin_type_locationsearch)
|
||||||
|
PluginType.Calendar -> stringResource(R.string.plugin_type_calendar)
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
|||||||
@ -116,28 +116,13 @@ fun SearchSettingsScreen() {
|
|||||||
enabled = hasContactsPermission == true
|
enabled = hasContactsPermission == true
|
||||||
)
|
)
|
||||||
|
|
||||||
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsStateWithLifecycle(
|
Preference(
|
||||||
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(
|
|
||||||
title = stringResource(R.string.preference_search_calendar),
|
title = stringResource(R.string.preference_search_calendar),
|
||||||
summary = stringResource(R.string.preference_search_calendar_summary),
|
summary = stringResource(R.string.preference_search_calendar_summary),
|
||||||
icon = Icons.Rounded.Today,
|
icon = Icons.Rounded.Today,
|
||||||
value = calendar == true && hasCalendarPermission == true,
|
onClick = {
|
||||||
onValueChanged = {
|
navController?.navigate("settings/search/calendar")
|
||||||
viewModel.setCalendar(it)
|
|
||||||
},
|
},
|
||||||
enabled = hasCalendarPermission == true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(
|
val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(
|
||||||
|
|||||||
@ -61,19 +61,6 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
permissionsManager.requestPermission(activity, PermissionGroup.Contacts)
|
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
|
val calculator = calculatorSearchSettings.enabled
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import androidx.lifecycle.Lifecycle
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
|
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
|
||||||
import de.mm20.launcher2.plugin.PluginState
|
import de.mm20.launcher2.plugin.PluginState
|
||||||
import de.mm20.launcher2.ui.BuildConfig
|
import de.mm20.launcher2.ui.BuildConfig
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
@ -58,7 +59,7 @@ fun WeatherIntegrationSettingsScreen() {
|
|||||||
primaryAction = {
|
primaryAction = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
try {
|
try {
|
||||||
state.setupActivity.send()
|
state.setupActivity.sendWithBackgroundPermission(context)
|
||||||
} catch (e: PendingIntent.CanceledException) {
|
} catch (e: PendingIntent.CanceledException) {
|
||||||
CrashReporter.logException(e)
|
CrashReporter.logException(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ abstract class QueryPluginApi<TQuery, TResult>(
|
|||||||
val uri = Uri.Builder()
|
val uri = Uri.Builder()
|
||||||
.scheme("content")
|
.scheme("content")
|
||||||
.authority(pluginAuthority)
|
.authority(pluginAuthority)
|
||||||
.path(LocationPluginContract.Paths.Search)
|
.path(SearchPluginContract.Paths.Search)
|
||||||
.appendQueryParameters(query)
|
.appendQueryParameters(query)
|
||||||
.appendQueryParameter(
|
.appendQueryParameter(
|
||||||
SearchPluginContract.Params.AllowNetwork,
|
SearchPluginContract.Params.AllowNetwork,
|
||||||
|
|||||||
@ -6,14 +6,17 @@ import de.mm20.launcher2.icons.StaticLauncherIcon
|
|||||||
import de.mm20.launcher2.icons.TextLayer
|
import de.mm20.launcher2.icons.TextLayer
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
interface CalendarEvent: SavableSearchable {
|
interface CalendarEvent : SavableSearchable {
|
||||||
val color: Int?
|
val color: Int?
|
||||||
val startTime: Long
|
val startTime: Long?
|
||||||
val endTime: Long
|
val endTime: Long
|
||||||
val allDay: Boolean
|
val allDay: Boolean
|
||||||
val description: String?
|
val description: String?
|
||||||
|
val calendarName: String?
|
||||||
val location: String?
|
val location: String?
|
||||||
val attendees: List<String>
|
val attendees: List<String>
|
||||||
|
val isCompleted: Boolean?
|
||||||
|
get() = null
|
||||||
|
|
||||||
|
|
||||||
override val preferDetailsOverLaunch: Boolean
|
override val preferDetailsOverLaunch: Boolean
|
||||||
@ -24,7 +27,7 @@ interface CalendarEvent: SavableSearchable {
|
|||||||
val df = SimpleDateFormat("dd")
|
val df = SimpleDateFormat("dd")
|
||||||
return StaticLauncherIcon(
|
return StaticLauncherIcon(
|
||||||
foregroundLayer = TextLayer(
|
foregroundLayer = TextLayer(
|
||||||
text = df.format(startTime),
|
text = df.format(startTime ?: endTime),
|
||||||
color = color ?: 0,
|
color = color ?: 0,
|
||||||
),
|
),
|
||||||
backgroundLayer = ColorLayer(color ?: 0)
|
backgroundLayer = ColorLayer(color ?: 0)
|
||||||
|
|||||||
@ -848,6 +848,7 @@
|
|||||||
<string name="search_filter_online">Online results</string>
|
<string name="search_filter_online">Online results</string>
|
||||||
<string name="search_filter_apps">Apps</string>
|
<string name="search_filter_apps">Apps</string>
|
||||||
<string name="plugin_type_locationsearch">Places search</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">Default filter</string>
|
||||||
<string name="preference_default_filter_summary">Customize the default filter for searches</string>
|
<string name="preference_default_filter_summary">Customize the default filter for searches</string>
|
||||||
<string name="preference_filter_bar">Show filter bar</string>
|
<string name="preference_filter_bar">Show filter bar</string>
|
||||||
|
|||||||
@ -53,7 +53,9 @@ data class LauncherSettingsData internal constructor(
|
|||||||
|
|
||||||
val contactSearchEnabled: Boolean = true,
|
val contactSearchEnabled: Boolean = true,
|
||||||
|
|
||||||
|
@Deprecated("Use calendarSearchProviders `local` instead")
|
||||||
val calendarSearchEnabled: Boolean = true,
|
val calendarSearchEnabled: Boolean = true,
|
||||||
|
val calendarSearchProviders: Set<String> = setOf("local"),
|
||||||
|
|
||||||
val shortcutSearchEnabled: Boolean = true,
|
val shortcutSearchEnabled: Boolean = true,
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,20 @@ import kotlinx.coroutines.flow.map
|
|||||||
|
|
||||||
class CalendarSearchSettings internal constructor(
|
class CalendarSearchSettings internal constructor(
|
||||||
private val dataStore: LauncherDataStore,
|
private val dataStore: LauncherDataStore,
|
||||||
){
|
) {
|
||||||
val enabled
|
val providers
|
||||||
get() = dataStore.data.map { it.calendarSearchEnabled }
|
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 {
|
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
|
package de.mm20.launcher2.plugin.contracts
|
||||||
|
|
||||||
abstract class CalendarPluginContract {
|
abstract class CalendarPluginContract {
|
||||||
object EventColumns: Columns() {
|
object EventColumns : Columns() {
|
||||||
/**
|
/**
|
||||||
* The unique ID of the event.
|
* The unique ID of the event.
|
||||||
*/
|
*/
|
||||||
@ -17,6 +17,11 @@ abstract class CalendarPluginContract {
|
|||||||
*/
|
*/
|
||||||
val Description = column<String>("description")
|
val Description = column<String>("description")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The calendar name of the event.
|
||||||
|
*/
|
||||||
|
val CalendarName = column<String>("calendar_name")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The location of the event.
|
* The location of the event.
|
||||||
*/
|
*/
|
||||||
@ -33,9 +38,9 @@ abstract class CalendarPluginContract {
|
|||||||
val EndTime = column<Long>("end_time")
|
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.
|
* The color of the event.
|
||||||
@ -46,5 +51,34 @@ abstract class CalendarPluginContract {
|
|||||||
* The attendees of the event.
|
* The attendees of the event.
|
||||||
*/
|
*/
|
||||||
val Attendees = column<List<String>>("attendees")
|
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 {
|
plugins {
|
||||||
alias(libs.plugins.android.library)
|
alias(libs.plugins.android.library)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.plugin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@ -1,42 +1,51 @@
|
|||||||
package de.mm20.launcher2.calendar
|
package de.mm20.launcher2.calendar
|
||||||
|
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.CalendarContract
|
import de.mm20.launcher2.calendar.providers.AndroidCalendarProvider
|
||||||
import androidx.core.database.getStringOrNull
|
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.PermissionGroup
|
||||||
import de.mm20.launcher2.permissions.PermissionsManager
|
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.preferences.search.CalendarSearchSettings
|
||||||
import de.mm20.launcher2.search.CalendarEvent
|
import de.mm20.launcher2.search.CalendarEvent
|
||||||
import de.mm20.launcher2.search.SearchableRepository
|
import de.mm20.launcher2.search.SearchableRepository
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.combine
|
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.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.flow.transform
|
||||||
import java.util.Calendar
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
interface CalendarRepository : SearchableRepository<CalendarEvent> {
|
interface CalendarRepository : SearchableRepository<CalendarEvent> {
|
||||||
fun findMany(
|
fun findMany(
|
||||||
from: Long = System.currentTimeMillis(),
|
from: Long = System.currentTimeMillis(),
|
||||||
to: Long = from + 14 * 24 * 60 * 60 * 1000L,
|
to: Long = from + 14 * 24 * 60 * 60 * 1000L,
|
||||||
excludeCalendars: List<Long> = emptyList(),
|
excludeCalendars: List<String> = emptyList(),
|
||||||
excludeAllDayEvents: Boolean = false,
|
excludeAllDayEvents: Boolean = false,
|
||||||
limit: Int = 999,
|
|
||||||
): Flow<ImmutableList<CalendarEvent>>
|
): Flow<ImmutableList<CalendarEvent>>
|
||||||
|
|
||||||
suspend fun getCalendars(): List<UserCalendar>
|
fun getCalendars(providerId: String? = null): Flow<List<CalendarList>>
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class CalendarRepositoryImpl(
|
internal class CalendarRepositoryImpl(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val permissionsManager: PermissionsManager,
|
private val permissionsManager: PermissionsManager,
|
||||||
|
private val pluginRepository: PluginRepository,
|
||||||
private val settings: CalendarSearchSettings,
|
private val settings: CalendarSearchSettings,
|
||||||
) : CalendarRepository {
|
) : CalendarRepository {
|
||||||
|
|
||||||
@ -48,48 +57,62 @@ internal class CalendarRepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||||
val enabled = settings.enabled
|
val providerIds = settings.providers
|
||||||
|
|
||||||
return hasPermission.combine(enabled) { a, b -> a && b }
|
return combineTransform(hasPermission, providerIds) { perm, providerIds ->
|
||||||
.map {
|
val providers = providerIds.mapNotNull {
|
||||||
if (it) {
|
when (it) {
|
||||||
val now = System.currentTimeMillis()
|
"local" -> if (perm) AndroidCalendarProvider(context) else null
|
||||||
queryCalendarEvents(
|
else -> PluginCalendarProvider(context, it)
|
||||||
query,
|
|
||||||
intervalStart = now,
|
|
||||||
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
|
|
||||||
).toImmutableList()
|
|
||||||
} else {
|
|
||||||
persistentListOf()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
emitAll(
|
||||||
|
queryCalendarEvents(
|
||||||
|
query = query,
|
||||||
|
excludeAllDayEvents = false,
|
||||||
|
providers = providers,
|
||||||
|
intervalStart = now,
|
||||||
|
intervalEnd = now + 730.days.inWholeMilliseconds,
|
||||||
|
allowNetwork = allowNetwork,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findMany(
|
override fun findMany(
|
||||||
from: Long,
|
from: Long,
|
||||||
to: Long,
|
to: Long,
|
||||||
excludeCalendars: List<Long>,
|
excludeCalendars: List<String>,
|
||||||
excludeAllDayEvents: Boolean,
|
excludeAllDayEvents: Boolean,
|
||||||
limit: Int,
|
): Flow<ImmutableList<CalendarEvent>> {
|
||||||
) = channelFlow<ImmutableList<CalendarEvent>> {
|
|
||||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||||
hasPermission.collectLatest {
|
val plugins = pluginRepository.findMany(
|
||||||
if (it) {
|
type = PluginType.Calendar,
|
||||||
val events = withContext(Dispatchers.IO) {
|
enabled = true,
|
||||||
queryCalendarEvents(
|
)
|
||||||
query = null,
|
return combineTransform(hasPermission, plugins) { perm, plugins ->
|
||||||
intervalStart = from,
|
val providers = buildList {
|
||||||
intervalEnd = to,
|
if (perm) add(AndroidCalendarProvider(context)) else null
|
||||||
limit = limit,
|
addAll(
|
||||||
excludeAllDayEvents = excludeAllDayEvents,
|
plugins.map {
|
||||||
excludeCalendars = excludeCalendars
|
PluginCalendarProvider(context, it.authority)
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
send(events.toImmutableList())
|
|
||||||
} else {
|
|
||||||
send(persistentListOf())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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?,
|
query: String?,
|
||||||
intervalStart: Long,
|
intervalStart: Long,
|
||||||
intervalEnd: Long,
|
intervalEnd: Long,
|
||||||
limit: Int = 10,
|
|
||||||
excludeAllDayEvents: Boolean = false,
|
excludeAllDayEvents: Boolean = false,
|
||||||
excludeCalendars: List<Long> = emptyList(),
|
excludeCalendars: List<String> = emptyList(),
|
||||||
): List<AndroidCalendarEvent> {
|
allowNetwork: Boolean = false,
|
||||||
val results = withContext(Dispatchers.IO) {
|
providers: List<CalendarProvider>,
|
||||||
val results = mutableListOf<AndroidCalendarEvent>()
|
): Flow<ImmutableList<CalendarEvent>> = flow {
|
||||||
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
|
supervisorScope {
|
||||||
ContentUris.appendId(builder, intervalStart)
|
val result = MutableStateFlow(persistentListOf<CalendarEvent>())
|
||||||
ContentUris.appendId(builder, intervalEnd)
|
|
||||||
val uri = builder.build()
|
for (provider in providers) {
|
||||||
val projection = arrayOf(
|
launch {
|
||||||
CalendarContract.Instances.EVENT_ID,
|
val r = provider.search(
|
||||||
CalendarContract.Instances.TITLE,
|
query,
|
||||||
CalendarContract.Instances.BEGIN,
|
from = intervalStart,
|
||||||
CalendarContract.Instances.END,
|
to = intervalEnd,
|
||||||
CalendarContract.Instances.ALL_DAY,
|
excludedCalendars = excludeCalendars.mapNotNull {
|
||||||
CalendarContract.Instances.DISPLAY_COLOR,
|
val (namespace, id) = it.split(":")
|
||||||
CalendarContract.Instances.EVENT_LOCATION,
|
if (namespace == provider.namespace) id else null
|
||||||
CalendarContract.Instances.CALENDAR_ID,
|
},
|
||||||
CalendarContract.Instances.DESCRIPTION
|
excludeAllDayEvents = excludeAllDayEvents,
|
||||||
)
|
allowNetwork = allowNetwork,
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
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()
|
emitAll(result)
|
||||||
return@withContext results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getCalendars(): List<UserCalendar> {
|
override fun getCalendars(providerId: String?): Flow<List<CalendarList>> {
|
||||||
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Calendar)) return emptyList()
|
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val calendars = mutableListOf<UserCalendar>()
|
val providers: Flow<List<CalendarProvider>> = if (providerId != null) {
|
||||||
val uri = CalendarContract.Calendars.CONTENT_URI
|
when(providerId) {
|
||||||
val proj = arrayOf(
|
"local" -> hasPermission.map { if (it) listOf(AndroidCalendarProvider(context)) else emptyList() }
|
||||||
CalendarContract.Calendars._ID,
|
else -> pluginRepository.get(providerId).map { if (it?.enabled == true) listOf(PluginCalendarProvider(context, providerId)) else emptyList() }
|
||||||
CalendarContract.Calendars.NAME,
|
}
|
||||||
CalendarContract.Calendars.ACCOUNT_NAME,
|
} else {
|
||||||
CalendarContract.Calendars.CALENDAR_COLOR,
|
val plugins = pluginRepository.findMany(
|
||||||
CalendarContract.Calendars.VISIBLE,
|
type = PluginType.Calendar,
|
||||||
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
|
enabled = true,
|
||||||
)
|
)
|
||||||
val cursor = context.contentResolver.query(uri, proj, null, null, null)
|
combine(hasPermission, plugins) { perm, plugins ->
|
||||||
?: return@withContext emptyList()
|
buildList {
|
||||||
while (cursor.moveToNext()) {
|
if (perm) add(AndroidCalendarProvider(context))
|
||||||
try {
|
addAll(plugins.map { PluginCalendarProvider(context, it.authority) })
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
package de.mm20.launcher2.calendar
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
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.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.SavableSearchable
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
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 org.json.JSONObject
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class CalendarEventSerializer: SearchableSerializer {
|
class AndroidCalendarEventSerializer: SearchableSerializer {
|
||||||
override fun serialize(searchable: SavableSearchable): String {
|
override fun serialize(searchable: SavableSearchable): String {
|
||||||
searchable as AndroidCalendarEvent
|
searchable as AndroidCalendarEvent
|
||||||
val json = JSONObject()
|
val json = JSONObject()
|
||||||
@ -22,84 +34,117 @@ class CalendarEventSerializer: SearchableSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val typePrefix: String
|
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? {
|
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
|
||||||
val json = JSONObject(serialized)
|
val json = JSONObject(serialized)
|
||||||
val id = json.getLong("id")
|
val id = json.getLong("id")
|
||||||
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
|
return AndroidCalendarProvider(context).get(id)
|
||||||
ContentUris.appendId(builder, System.currentTimeMillis())
|
}
|
||||||
ContentUris.appendId(builder, System.currentTimeMillis() + 63072000000L)
|
|
||||||
val uri = builder.build()
|
}
|
||||||
val projection = arrayOf(
|
|
||||||
CalendarContract.Instances.EVENT_ID,
|
@Serializable
|
||||||
CalendarContract.Instances.TITLE,
|
internal data class SerializedCalendarEvent(
|
||||||
CalendarContract.Instances.BEGIN,
|
val id: String? = null,
|
||||||
CalendarContract.Instances.END,
|
val authority: String? = null,
|
||||||
CalendarContract.Instances.ALL_DAY,
|
val color: Int? = null,
|
||||||
CalendarContract.Instances.DISPLAY_COLOR,
|
val startTime: Long? = null,
|
||||||
CalendarContract.Instances.EVENT_LOCATION,
|
val endTime: Long? = null,
|
||||||
CalendarContract.Instances.CALENDAR_ID,
|
val allDay: Boolean? = null,
|
||||||
CalendarContract.Instances.DESCRIPTION
|
val description: String? = null,
|
||||||
)
|
val calendarName: String? = null,
|
||||||
val selection = CalendarContract.Instances.EVENT_ID + " = ?"
|
val location: String? = null,
|
||||||
val selArgs = arrayOf(id.toString())
|
val attendees: List<String>? = null,
|
||||||
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, null)
|
val uri: String? = null,
|
||||||
?: return null
|
val completed: Boolean? = null,
|
||||||
if (cursor.moveToNext()) {
|
val label: String? = null,
|
||||||
val title = cursor.getStringOrNull(1) ?: ""
|
val timestamp: Long = 0L,
|
||||||
val begin = cursor.getLong(2)
|
val strategy: StorageStrategy = StorageStrategy.StoreCopy,
|
||||||
val end = cursor.getLong(3)
|
)
|
||||||
val allday = cursor.getInt(4) != 0
|
|
||||||
val color = cursor.getInt(5)
|
class PluginCalendarEventSerializer: SearchableSerializer {
|
||||||
val location = cursor.getStringOrNull(6)
|
override fun serialize(searchable: SavableSearchable): String {
|
||||||
val calendar = cursor.getLong(7)
|
searchable as PluginCalendarEvent
|
||||||
val description = cursor.getStringOrNull(8)
|
if (searchable.storageStrategy == StorageStrategy.StoreCopy) {
|
||||||
?: ""
|
return Json.Lenient.encodeToString(
|
||||||
cursor.close()
|
SerializedCalendarEvent(
|
||||||
val proj = arrayOf(
|
id = searchable.id,
|
||||||
CalendarContract.Attendees.EVENT_ID,
|
authority = searchable.authority,
|
||||||
CalendarContract.Attendees.ATTENDEE_NAME,
|
color = searchable.color,
|
||||||
CalendarContract.Attendees.ATTENDEE_EMAIL
|
startTime = searchable.startTime,
|
||||||
)
|
endTime = searchable.endTime,
|
||||||
val sel = "${CalendarContract.Attendees.EVENT_ID} = $id"
|
allDay = searchable.allDay,
|
||||||
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
|
description = searchable.description,
|
||||||
val cur = context.contentResolver.query(
|
calendarName = searchable.calendarName,
|
||||||
CalendarContract.Attendees.CONTENT_URI,
|
location = searchable.location,
|
||||||
proj, sel, null, s
|
attendees = searchable.attendees,
|
||||||
) ?: return null
|
uri = searchable.uri.toString(),
|
||||||
val attendees = mutableListOf<String>()
|
completed = searchable.isCompleted,
|
||||||
while (cur.moveToNext()) {
|
label = searchable.label,
|
||||||
attendees.add(
|
strategy = searchable.storageStrategy,
|
||||||
cur.getStringOrNull(1).takeUnless { it.isNullOrBlank() }
|
timestamp = searchable.timestamp,
|
||||||
?: cur.getStringOrNull(2)
|
)
|
||||||
?: continue
|
)
|
||||||
|
} 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
|
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.CalendarEvent
|
||||||
import de.mm20.launcher2.search.SearchableDeserializer
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.SearchableRepository
|
import de.mm20.launcher2.search.SearchableRepository
|
||||||
@ -9,6 +11,7 @@ import org.koin.dsl.module
|
|||||||
|
|
||||||
val calendarModule = module {
|
val calendarModule = module {
|
||||||
factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { get<CalendarRepository>() }
|
factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { get<CalendarRepository>() }
|
||||||
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get(), get()) }
|
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get(), get(), get()) }
|
||||||
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { CalendarEventDeserializer(androidContext()) }
|
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.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@ -6,6 +6,7 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.CalendarContract
|
import android.provider.CalendarContract
|
||||||
|
import de.mm20.launcher2.calendar.AndroidCalendarEventSerializer
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
import de.mm20.launcher2.search.CalendarEvent
|
import de.mm20.launcher2.search.CalendarEvent
|
||||||
import de.mm20.launcher2.search.SearchableSerializer
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
@ -21,7 +22,8 @@ internal data class AndroidCalendarEvent(
|
|||||||
override val location: String?,
|
override val location: String?,
|
||||||
override val attendees: List<String>,
|
override val attendees: List<String>,
|
||||||
override val description: String?,
|
override val description: String?,
|
||||||
val calendar: Long,
|
internal val calendarId: Long,
|
||||||
|
override val calendarName: String?,
|
||||||
override val labelOverride: String? = null,
|
override val labelOverride: String? = null,
|
||||||
) : CalendarEvent {
|
) : CalendarEvent {
|
||||||
|
|
||||||
@ -61,7 +63,7 @@ internal data class AndroidCalendarEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getSerializer(): SearchableSerializer {
|
override fun getSerializer(): SearchableSerializer {
|
||||||
return CalendarEventSerializer()
|
return AndroidCalendarEventSerializer()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -69,9 +71,10 @@ internal data class AndroidCalendarEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UserCalendar(
|
data class CalendarList(
|
||||||
val id: Long,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val owner: String,
|
val owner: String?,
|
||||||
val color: Int
|
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
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,7 @@
|
|||||||
package de.mm20.launcher2.widgets
|
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.database.entities.PartialWidgetEntity
|
||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -15,7 +10,11 @@ import java.util.UUID
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class CalendarWidgetConfig(
|
data class CalendarWidgetConfig(
|
||||||
val allDayEvents: Boolean = true,
|
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(
|
data class CalendarWidget(
|
||||||
override val id: UUID,
|
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