Add calendar plugins
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.plugin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@@ -1,42 +1,51 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.provider.CalendarContract
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarProvider
|
||||
import de.mm20.launcher2.calendar.providers.CalendarList
|
||||
import de.mm20.launcher2.calendar.providers.CalendarProvider
|
||||
import de.mm20.launcher2.calendar.providers.PluginCalendarProvider
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Calendar
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
interface CalendarRepository : SearchableRepository<CalendarEvent> {
|
||||
fun findMany(
|
||||
from: Long = System.currentTimeMillis(),
|
||||
to: Long = from + 14 * 24 * 60 * 60 * 1000L,
|
||||
excludeCalendars: List<Long> = emptyList(),
|
||||
excludeCalendars: List<String> = emptyList(),
|
||||
excludeAllDayEvents: Boolean = false,
|
||||
limit: Int = 999,
|
||||
): Flow<ImmutableList<CalendarEvent>>
|
||||
|
||||
suspend fun getCalendars(): List<UserCalendar>
|
||||
fun getCalendars(providerId: String? = null): Flow<List<CalendarList>>
|
||||
}
|
||||
|
||||
internal class CalendarRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val permissionsManager: PermissionsManager,
|
||||
private val pluginRepository: PluginRepository,
|
||||
private val settings: CalendarSearchSettings,
|
||||
) : CalendarRepository {
|
||||
|
||||
@@ -48,48 +57,62 @@ internal class CalendarRepositoryImpl(
|
||||
}
|
||||
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
val enabled = settings.enabled
|
||||
val providerIds = settings.providers
|
||||
|
||||
return hasPermission.combine(enabled) { a, b -> a && b }
|
||||
.map {
|
||||
if (it) {
|
||||
val now = System.currentTimeMillis()
|
||||
queryCalendarEvents(
|
||||
query,
|
||||
intervalStart = now,
|
||||
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
|
||||
).toImmutableList()
|
||||
} else {
|
||||
persistentListOf()
|
||||
return combineTransform(hasPermission, providerIds) { perm, providerIds ->
|
||||
val providers = providerIds.mapNotNull {
|
||||
when (it) {
|
||||
"local" -> if (perm) AndroidCalendarProvider(context) else null
|
||||
else -> PluginCalendarProvider(context, it)
|
||||
}
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
emitAll(
|
||||
queryCalendarEvents(
|
||||
query = query,
|
||||
excludeAllDayEvents = false,
|
||||
providers = providers,
|
||||
intervalStart = now,
|
||||
intervalEnd = now + 730.days.inWholeMilliseconds,
|
||||
allowNetwork = allowNetwork,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findMany(
|
||||
from: Long,
|
||||
to: Long,
|
||||
excludeCalendars: List<Long>,
|
||||
excludeCalendars: List<String>,
|
||||
excludeAllDayEvents: Boolean,
|
||||
limit: Int,
|
||||
) = channelFlow<ImmutableList<CalendarEvent>> {
|
||||
): Flow<ImmutableList<CalendarEvent>> {
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
hasPermission.collectLatest {
|
||||
if (it) {
|
||||
val events = withContext(Dispatchers.IO) {
|
||||
queryCalendarEvents(
|
||||
query = null,
|
||||
intervalStart = from,
|
||||
intervalEnd = to,
|
||||
limit = limit,
|
||||
excludeAllDayEvents = excludeAllDayEvents,
|
||||
excludeCalendars = excludeCalendars
|
||||
)
|
||||
}
|
||||
send(events.toImmutableList())
|
||||
} else {
|
||||
send(persistentListOf())
|
||||
val plugins = pluginRepository.findMany(
|
||||
type = PluginType.Calendar,
|
||||
enabled = true,
|
||||
)
|
||||
return combineTransform(hasPermission, plugins) { perm, plugins ->
|
||||
val providers = buildList {
|
||||
if (perm) add(AndroidCalendarProvider(context)) else null
|
||||
addAll(
|
||||
plugins.map {
|
||||
PluginCalendarProvider(context, it.authority)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
emitAll(
|
||||
queryCalendarEvents(
|
||||
query = null,
|
||||
intervalStart = from,
|
||||
intervalEnd = to,
|
||||
excludeAllDayEvents = excludeAllDayEvents,
|
||||
excludeCalendars = excludeCalendars,
|
||||
providers = providers,
|
||||
allowNetwork = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,124 +120,70 @@ internal class CalendarRepositoryImpl(
|
||||
query: String?,
|
||||
intervalStart: Long,
|
||||
intervalEnd: Long,
|
||||
limit: Int = 10,
|
||||
excludeAllDayEvents: Boolean = false,
|
||||
excludeCalendars: List<Long> = emptyList(),
|
||||
): List<AndroidCalendarEvent> {
|
||||
val results = withContext(Dispatchers.IO) {
|
||||
val results = mutableListOf<AndroidCalendarEvent>()
|
||||
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
|
||||
ContentUris.appendId(builder, intervalStart)
|
||||
ContentUris.appendId(builder, intervalEnd)
|
||||
val uri = builder.build()
|
||||
val projection = arrayOf(
|
||||
CalendarContract.Instances.EVENT_ID,
|
||||
CalendarContract.Instances.TITLE,
|
||||
CalendarContract.Instances.BEGIN,
|
||||
CalendarContract.Instances.END,
|
||||
CalendarContract.Instances.ALL_DAY,
|
||||
CalendarContract.Instances.DISPLAY_COLOR,
|
||||
CalendarContract.Instances.EVENT_LOCATION,
|
||||
CalendarContract.Instances.CALENDAR_ID,
|
||||
CalendarContract.Instances.DESCRIPTION
|
||||
)
|
||||
val selection = mutableListOf<String>()
|
||||
if (query != null) selection.add("${CalendarContract.Instances.TITLE} LIKE ?")
|
||||
if (excludeCalendars.isNotEmpty()) selection.add("${CalendarContract.Instances.CALENDAR_ID} NOT IN (${excludeCalendars.joinToString()})")
|
||||
if (excludeAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0")
|
||||
val selArgs = if (query != null) arrayOf("%$query%") else null
|
||||
val sort =
|
||||
"${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else ""
|
||||
val cursor = context.contentResolver.query(
|
||||
uri,
|
||||
projection,
|
||||
selection.joinToString(separator = " AND "),
|
||||
selArgs,
|
||||
sort
|
||||
) ?: return@withContext mutableListOf()
|
||||
val proj = arrayOf(
|
||||
CalendarContract.Attendees.EVENT_ID,
|
||||
CalendarContract.Attendees.ATTENDEE_NAME,
|
||||
CalendarContract.Attendees.ATTENDEE_EMAIL
|
||||
)
|
||||
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
|
||||
while (cursor.moveToNext()) {
|
||||
val sel = "${CalendarContract.Attendees.EVENT_ID} = ${cursor.getLong(0)}"
|
||||
val cur = context.contentResolver.query(
|
||||
CalendarContract.Attendees.CONTENT_URI,
|
||||
proj, sel, null, s
|
||||
) ?: return@withContext mutableListOf()
|
||||
val attendees = mutableListOf<String>()
|
||||
while (cur.moveToNext()) {
|
||||
attendees.add(
|
||||
cur.getStringOrNull(1).takeUnless { it.isNullOrBlank() }
|
||||
?: cur.getStringOrNull(2)
|
||||
?: continue
|
||||
excludeCalendars: List<String> = emptyList(),
|
||||
allowNetwork: Boolean = false,
|
||||
providers: List<CalendarProvider>,
|
||||
): Flow<ImmutableList<CalendarEvent>> = flow {
|
||||
supervisorScope {
|
||||
val result = MutableStateFlow(persistentListOf<CalendarEvent>())
|
||||
|
||||
for (provider in providers) {
|
||||
launch {
|
||||
val r = provider.search(
|
||||
query,
|
||||
from = intervalStart,
|
||||
to = intervalEnd,
|
||||
excludedCalendars = excludeCalendars.mapNotNull {
|
||||
val (namespace, id) = it.split(":")
|
||||
if (namespace == provider.namespace) id else null
|
||||
},
|
||||
excludeAllDayEvents = excludeAllDayEvents,
|
||||
allowNetwork = allowNetwork,
|
||||
)
|
||||
result.update {
|
||||
(it + r).toPersistentList()
|
||||
}
|
||||
}
|
||||
cur.close()
|
||||
val allday = cursor.getInt(4) > 0
|
||||
val begin = cursor.getLong(2)
|
||||
|
||||
val tzOffset = if (allday) {
|
||||
Calendar.getInstance().timeZone.getOffset(begin)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val event = AndroidCalendarEvent(
|
||||
label = cursor.getStringOrNull(1) ?: "",
|
||||
id = cursor.getLong(0),
|
||||
color = cursor.getInt(5),
|
||||
startTime = begin - tzOffset,
|
||||
endTime = cursor.getLong(3) - tzOffset - if (allday) 1 else 0,
|
||||
allDay = allday,
|
||||
location = cursor.getStringOrNull(6) ?: "",
|
||||
attendees = attendees,
|
||||
description = cursor.getStringOrNull(8)
|
||||
?: "",
|
||||
calendar = cursor.getLong(7)
|
||||
)
|
||||
results.add(event)
|
||||
}
|
||||
cursor.close()
|
||||
return@withContext results
|
||||
emitAll(result)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
override suspend fun getCalendars(): List<UserCalendar> {
|
||||
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Calendar)) return emptyList()
|
||||
return withContext(Dispatchers.IO) {
|
||||
val calendars = mutableListOf<UserCalendar>()
|
||||
val uri = CalendarContract.Calendars.CONTENT_URI
|
||||
val proj = arrayOf(
|
||||
CalendarContract.Calendars._ID,
|
||||
CalendarContract.Calendars.NAME,
|
||||
CalendarContract.Calendars.ACCOUNT_NAME,
|
||||
CalendarContract.Calendars.CALENDAR_COLOR,
|
||||
CalendarContract.Calendars.VISIBLE,
|
||||
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
|
||||
override fun getCalendars(providerId: String?): Flow<List<CalendarList>> {
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
|
||||
val providers: Flow<List<CalendarProvider>> = if (providerId != null) {
|
||||
when(providerId) {
|
||||
"local" -> hasPermission.map { if (it) listOf(AndroidCalendarProvider(context)) else emptyList() }
|
||||
else -> pluginRepository.get(providerId).map { if (it?.enabled == true) listOf(PluginCalendarProvider(context, providerId)) else emptyList() }
|
||||
}
|
||||
} else {
|
||||
val plugins = pluginRepository.findMany(
|
||||
type = PluginType.Calendar,
|
||||
enabled = true,
|
||||
)
|
||||
val cursor = context.contentResolver.query(uri, proj, null, null, null)
|
||||
?: return@withContext emptyList()
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
calendars.add(
|
||||
UserCalendar(
|
||||
id = cursor.getLong(0),
|
||||
name = cursor.getStringOrNull(5) ?: cursor.getStringOrNull(1) ?: "",
|
||||
owner = cursor.getStringOrNull(2) ?: "",
|
||||
color = cursor.getInt(3)
|
||||
)
|
||||
)
|
||||
} catch (e: NullPointerException) {
|
||||
continue
|
||||
combine(hasPermission, plugins) { perm, plugins ->
|
||||
buildList {
|
||||
if (perm) add(AndroidCalendarProvider(context))
|
||||
addAll(plugins.map { PluginCalendarProvider(context, it.authority) })
|
||||
}
|
||||
}
|
||||
cursor.close()
|
||||
calendars.sortBy { it.owner }
|
||||
return@withContext calendars
|
||||
}
|
||||
|
||||
return providers.transform { providers ->
|
||||
supervisorScope {
|
||||
val result = MutableStateFlow(emptyList<CalendarList>())
|
||||
for (provider in providers) {
|
||||
launch {
|
||||
val r = provider.getCalendarLists()
|
||||
result.update { it + r }
|
||||
}
|
||||
}
|
||||
emitAll(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,31 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.provider.CalendarContract
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.database.getStringOrNull
|
||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarEvent
|
||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarProvider
|
||||
import de.mm20.launcher2.calendar.providers.PluginCalendarEvent
|
||||
import de.mm20.launcher2.calendar.providers.PluginCalendarProvider
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.UpdateResult
|
||||
import de.mm20.launcher2.search.asUpdateResult
|
||||
import de.mm20.launcher2.serialization.Json
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
class CalendarEventSerializer: SearchableSerializer {
|
||||
class AndroidCalendarEventSerializer: SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as AndroidCalendarEvent
|
||||
val json = JSONObject()
|
||||
@@ -22,84 +34,117 @@ class CalendarEventSerializer: SearchableSerializer {
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = "calendar"
|
||||
get() = AndroidCalendarEvent.Domain
|
||||
}
|
||||
|
||||
class CalendarEventDeserializer(val context: Context): SearchableDeserializer {
|
||||
class AndroidCalendarEventDeserializer(val context: Context): SearchableDeserializer {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
|
||||
val json = JSONObject(serialized)
|
||||
val id = json.getLong("id")
|
||||
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
|
||||
ContentUris.appendId(builder, System.currentTimeMillis())
|
||||
ContentUris.appendId(builder, System.currentTimeMillis() + 63072000000L)
|
||||
val uri = builder.build()
|
||||
val projection = arrayOf(
|
||||
CalendarContract.Instances.EVENT_ID,
|
||||
CalendarContract.Instances.TITLE,
|
||||
CalendarContract.Instances.BEGIN,
|
||||
CalendarContract.Instances.END,
|
||||
CalendarContract.Instances.ALL_DAY,
|
||||
CalendarContract.Instances.DISPLAY_COLOR,
|
||||
CalendarContract.Instances.EVENT_LOCATION,
|
||||
CalendarContract.Instances.CALENDAR_ID,
|
||||
CalendarContract.Instances.DESCRIPTION
|
||||
)
|
||||
val selection = CalendarContract.Instances.EVENT_ID + " = ?"
|
||||
val selArgs = arrayOf(id.toString())
|
||||
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, null)
|
||||
?: return null
|
||||
if (cursor.moveToNext()) {
|
||||
val title = cursor.getStringOrNull(1) ?: ""
|
||||
val begin = cursor.getLong(2)
|
||||
val end = cursor.getLong(3)
|
||||
val allday = cursor.getInt(4) != 0
|
||||
val color = cursor.getInt(5)
|
||||
val location = cursor.getStringOrNull(6)
|
||||
val calendar = cursor.getLong(7)
|
||||
val description = cursor.getStringOrNull(8)
|
||||
?: ""
|
||||
cursor.close()
|
||||
val proj = arrayOf(
|
||||
CalendarContract.Attendees.EVENT_ID,
|
||||
CalendarContract.Attendees.ATTENDEE_NAME,
|
||||
CalendarContract.Attendees.ATTENDEE_EMAIL
|
||||
return AndroidCalendarProvider(context).get(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class SerializedCalendarEvent(
|
||||
val id: String? = null,
|
||||
val authority: String? = null,
|
||||
val color: Int? = null,
|
||||
val startTime: Long? = null,
|
||||
val endTime: Long? = null,
|
||||
val allDay: Boolean? = null,
|
||||
val description: String? = null,
|
||||
val calendarName: String? = null,
|
||||
val location: String? = null,
|
||||
val attendees: List<String>? = null,
|
||||
val uri: String? = null,
|
||||
val completed: Boolean? = null,
|
||||
val label: String? = null,
|
||||
val timestamp: Long = 0L,
|
||||
val strategy: StorageStrategy = StorageStrategy.StoreCopy,
|
||||
)
|
||||
|
||||
class PluginCalendarEventSerializer: SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as PluginCalendarEvent
|
||||
if (searchable.storageStrategy == StorageStrategy.StoreCopy) {
|
||||
return Json.Lenient.encodeToString(
|
||||
SerializedCalendarEvent(
|
||||
id = searchable.id,
|
||||
authority = searchable.authority,
|
||||
color = searchable.color,
|
||||
startTime = searchable.startTime,
|
||||
endTime = searchable.endTime,
|
||||
allDay = searchable.allDay,
|
||||
description = searchable.description,
|
||||
calendarName = searchable.calendarName,
|
||||
location = searchable.location,
|
||||
attendees = searchable.attendees,
|
||||
uri = searchable.uri.toString(),
|
||||
completed = searchable.isCompleted,
|
||||
label = searchable.label,
|
||||
strategy = searchable.storageStrategy,
|
||||
timestamp = searchable.timestamp,
|
||||
)
|
||||
)
|
||||
val sel = "${CalendarContract.Attendees.EVENT_ID} = $id"
|
||||
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
|
||||
val cur = context.contentResolver.query(
|
||||
CalendarContract.Attendees.CONTENT_URI,
|
||||
proj, sel, null, s
|
||||
) ?: return null
|
||||
val attendees = mutableListOf<String>()
|
||||
while (cur.moveToNext()) {
|
||||
attendees.add(
|
||||
cur.getStringOrNull(1).takeUnless { it.isNullOrBlank() }
|
||||
?: cur.getStringOrNull(2)
|
||||
?: continue
|
||||
} else {
|
||||
return Json.Lenient.encodeToString(
|
||||
SerializedCalendarEvent(
|
||||
id = searchable.id,
|
||||
authority = searchable.authority,
|
||||
strategy = searchable.storageStrategy,
|
||||
)
|
||||
}
|
||||
cur.close()
|
||||
val tzOffset = if (allday) {
|
||||
Calendar.getInstance().timeZone.getOffset(begin)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
return AndroidCalendarEvent(
|
||||
label = title,
|
||||
id = id,
|
||||
color = color,
|
||||
startTime = begin - tzOffset,
|
||||
endTime = end - tzOffset - if (allday) 1 else 0,
|
||||
allDay = allday,
|
||||
location = location ?: "",
|
||||
attendees = attendees,
|
||||
description = description,
|
||||
calendar = calendar
|
||||
)
|
||||
}
|
||||
cursor.close()
|
||||
return null
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = PluginCalendarEvent.Domain
|
||||
}
|
||||
|
||||
class PluginCalendarEventDeserializer(
|
||||
val context: Context,
|
||||
private val pluginRepository: PluginRepository,
|
||||
): SearchableDeserializer {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
val json = Json.Lenient.decodeFromString<SerializedCalendarEvent>(serialized)
|
||||
val authority = json.authority ?: return null
|
||||
val id = json.id ?: return null
|
||||
val strategy = json.strategy
|
||||
val plugin = pluginRepository.get(authority).firstOrNull() ?: return null
|
||||
if (!plugin.enabled) return null
|
||||
|
||||
return when(strategy) {
|
||||
StorageStrategy.StoreReference -> {
|
||||
PluginCalendarProvider(context, authority).get(id).getOrNull()
|
||||
}
|
||||
else -> {
|
||||
val timestamp = json.timestamp
|
||||
PluginCalendarEvent(
|
||||
id = id,
|
||||
color = json.color,
|
||||
startTime = json.startTime ?: 0,
|
||||
endTime = json.endTime ?: 0,
|
||||
allDay = json.allDay ?: false,
|
||||
description = json.description,
|
||||
calendarName = json.calendarName,
|
||||
location = json.location,
|
||||
attendees = json.attendees ?: emptyList(),
|
||||
label = json.label ?: return null,
|
||||
uri = Uri.parse(json.uri ?: return null),
|
||||
isCompleted = json.completed,
|
||||
storageStrategy = StorageStrategy.StoreCopy,
|
||||
authority = authority,
|
||||
timestamp = timestamp,
|
||||
updatedSelf = {
|
||||
if (it !is PluginCalendarEvent) UpdateResult.TemporarilyUnavailable()
|
||||
else PluginCalendarProvider(context, authority).refresh(it, timestamp).asUpdateResult()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
|
||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarEvent
|
||||
import de.mm20.launcher2.calendar.providers.PluginCalendarEvent
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
@@ -9,6 +11,7 @@ import org.koin.dsl.module
|
||||
|
||||
val calendarModule = module {
|
||||
factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { get<CalendarRepository>() }
|
||||
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get(), get()) }
|
||||
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { CalendarEventDeserializer(androidContext()) }
|
||||
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get(), get(), get()) }
|
||||
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { AndroidCalendarEventDeserializer(androidContext()) }
|
||||
factory<SearchableDeserializer>(named(PluginCalendarEvent.Domain)) { PluginCalendarEventDeserializer(androidContext(), get()) }
|
||||
}
|
||||
+10
-7
@@ -1,4 +1,4 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
package de.mm20.launcher2.calendar.providers
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
@@ -6,6 +6,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import de.mm20.launcher2.calendar.AndroidCalendarEventSerializer
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
@@ -21,7 +22,8 @@ internal data class AndroidCalendarEvent(
|
||||
override val location: String?,
|
||||
override val attendees: List<String>,
|
||||
override val description: String?,
|
||||
val calendar: Long,
|
||||
internal val calendarId: Long,
|
||||
override val calendarName: String?,
|
||||
override val labelOverride: String? = null,
|
||||
) : CalendarEvent {
|
||||
|
||||
@@ -61,7 +63,7 @@ internal data class AndroidCalendarEvent(
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return CalendarEventSerializer()
|
||||
return AndroidCalendarEventSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -69,9 +71,10 @@ internal data class AndroidCalendarEvent(
|
||||
}
|
||||
}
|
||||
|
||||
data class UserCalendar(
|
||||
val id: Long,
|
||||
data class CalendarList(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val owner: String,
|
||||
val color: Int
|
||||
val owner: String?,
|
||||
val color: Int,
|
||||
val providerId: String,
|
||||
)
|
||||
+218
@@ -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
|
||||
}
|
||||
+65
@@ -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"
|
||||
}
|
||||
}
|
||||
+186
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user