Add tasks.org integration
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
package de.mm20.launcher2.calendar
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
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.calendar.providers.TasksCalendarProvider
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
@@ -21,10 +21,7 @@ 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.flow.transform
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -57,14 +54,21 @@ internal class CalendarRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
val hasTasksPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
|
||||
val providerIds = settings.providers
|
||||
val excludedCalendars = settings.excludedCalendars
|
||||
|
||||
return combineTransform(hasPermission, providerIds, excludedCalendars) { perm, providerIds, excludedCalendars ->
|
||||
return combineTransform(
|
||||
hasCalendarPermission,
|
||||
hasTasksPermission,
|
||||
providerIds,
|
||||
excludedCalendars
|
||||
) { calPerm, taskPerm, providerIds, excludedCalendars ->
|
||||
val providers = providerIds.mapNotNull {
|
||||
when (it) {
|
||||
"local" -> if (perm) AndroidCalendarProvider(context) else null
|
||||
"local" -> if (calPerm) AndroidCalendarProvider(context) else null
|
||||
"tasks.org" -> if (taskPerm) TasksCalendarProvider(context) else null
|
||||
else -> PluginCalendarProvider(context, it)
|
||||
}
|
||||
}
|
||||
@@ -90,14 +94,16 @@ internal class CalendarRepositoryImpl(
|
||||
excludeCalendars: List<String>,
|
||||
excludeAllDayEvents: Boolean,
|
||||
): Flow<ImmutableList<CalendarEvent>> {
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
val hasTasksPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
|
||||
val plugins = pluginRepository.findMany(
|
||||
type = PluginType.Calendar,
|
||||
enabled = true,
|
||||
)
|
||||
return combineTransform(hasPermission, plugins) { perm, plugins ->
|
||||
return combineTransform(hasCalendarPermission, hasTasksPermission, plugins) { calPerm, taskPerm, plugins ->
|
||||
val providers = buildList {
|
||||
if (perm) add(AndroidCalendarProvider(context)) else null
|
||||
if (calPerm) add(AndroidCalendarProvider(context)) else null
|
||||
if (taskPerm) add(TasksCalendarProvider(context)) else null
|
||||
addAll(
|
||||
plugins.map {
|
||||
PluginCalendarProvider(context, it.authority)
|
||||
@@ -119,7 +125,7 @@ internal class CalendarRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun queryCalendarEvents(
|
||||
private fun queryCalendarEvents(
|
||||
query: String?,
|
||||
intervalStart: Long,
|
||||
intervalEnd: Long,
|
||||
@@ -154,21 +160,42 @@ internal class CalendarRepositoryImpl(
|
||||
}
|
||||
|
||||
override fun getCalendars(providerId: String?): Flow<List<CalendarList>> {
|
||||
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
|
||||
val hasTaskPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
|
||||
|
||||
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() }
|
||||
when (providerId) {
|
||||
"local" -> hasCalendarPermission.map {
|
||||
if (it) listOf(
|
||||
AndroidCalendarProvider(
|
||||
context
|
||||
)
|
||||
) else emptyList()
|
||||
}
|
||||
|
||||
"tasks.org" -> hasTaskPermission.map { if (it) listOf(TasksCalendarProvider(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,
|
||||
)
|
||||
combine(hasPermission, plugins) { perm, plugins ->
|
||||
combine(
|
||||
hasCalendarPermission,
|
||||
hasTaskPermission,
|
||||
plugins
|
||||
) { calPerm, tasksPerm, plugins ->
|
||||
buildList {
|
||||
if (perm) add(AndroidCalendarProvider(context))
|
||||
if (calPerm) add(AndroidCalendarProvider(context))
|
||||
if (tasksPerm) add(TasksCalendarProvider(context))
|
||||
addAll(plugins.map { PluginCalendarProvider(context, it.authority) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ 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.calendar.providers.TasksCalendarEvent
|
||||
import de.mm20.launcher2.calendar.providers.TasksCalendarProvider
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
@@ -44,9 +46,31 @@ class AndroidCalendarEventDeserializer(val context: Context): SearchableDeserial
|
||||
val id = json.getLong("id")
|
||||
return AndroidCalendarProvider(context).get(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TasksCalendarEventSerializer: SearchableSerializer {
|
||||
override fun serialize(searchable: SavableSearchable): String {
|
||||
searchable as TasksCalendarEvent
|
||||
val json = JSONObject()
|
||||
json.put("id", searchable.id)
|
||||
return json.toString()
|
||||
}
|
||||
|
||||
override val typePrefix: String
|
||||
get() = TasksCalendarEvent.Domain
|
||||
}
|
||||
|
||||
class TasksCalendarEventDeserializer(val context: Context): SearchableDeserializer {
|
||||
override suspend fun deserialize(serialized: String): SavableSearchable? {
|
||||
if (ContextCompat.checkSelfPermission(context, "org.tasks.permission.READ_TASKS") != PackageManager.PERMISSION_GRANTED) return null
|
||||
val json = JSONObject(serialized)
|
||||
val id = json.getLong("id")
|
||||
return TasksCalendarProvider(context).get(id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Serializable
|
||||
internal data class SerializedCalendarEvent(
|
||||
val id: String? = null,
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.mm20.launcher2.calendar
|
||||
|
||||
import de.mm20.launcher2.calendar.providers.AndroidCalendarEvent
|
||||
import de.mm20.launcher2.calendar.providers.PluginCalendarEvent
|
||||
import de.mm20.launcher2.calendar.providers.TasksCalendarEvent
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableDeserializer
|
||||
import de.mm20.launcher2.search.SearchableRepository
|
||||
@@ -13,5 +14,6 @@ val calendarModule = module {
|
||||
factory<SearchableRepository<CalendarEvent>>(named<CalendarEvent>()) { get<CalendarRepository>() }
|
||||
factory<CalendarRepository> { CalendarRepositoryImpl(androidContext(), get(), get(), get()) }
|
||||
factory<SearchableDeserializer>(named(AndroidCalendarEvent.Domain)) { AndroidCalendarEventDeserializer(androidContext()) }
|
||||
factory<SearchableDeserializer>(named(TasksCalendarEvent.Domain)) { TasksCalendarEventDeserializer(androidContext()) }
|
||||
factory<SearchableDeserializer>(named(PluginCalendarEvent.Domain)) { PluginCalendarEventDeserializer(androidContext(), get()) }
|
||||
}
|
||||
+1
-18
@@ -11,6 +11,7 @@ import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import java.net.URLEncoder
|
||||
import androidx.core.net.toUri
|
||||
|
||||
internal data class AndroidCalendarEvent(
|
||||
override val label: String,
|
||||
@@ -44,24 +45,6 @@ internal data class AndroidCalendarEvent(
|
||||
return context.tryStartActivity(getLaunchIntent(), options)
|
||||
}
|
||||
|
||||
override fun openLocation(context: Context) {
|
||||
if (location == null) return
|
||||
context.tryStartActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.setData(
|
||||
Uri.parse(
|
||||
"geo:0,0?q=${
|
||||
URLEncoder.encode(
|
||||
location,
|
||||
"utf8"
|
||||
)
|
||||
}"
|
||||
)
|
||||
)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return AndroidCalendarEventSerializer()
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package de.mm20.launcher2.calendar.providers
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import androidx.core.net.toUri
|
||||
import de.mm20.launcher2.calendar.TasksCalendarEventSerializer
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
|
||||
data class TasksCalendarEvent(
|
||||
override val label: String,
|
||||
val id: Long,
|
||||
override val color: Int?,
|
||||
override val startTime: Long?,
|
||||
override val endTime: Long,
|
||||
override val allDay: Boolean,
|
||||
override val isCompleted: Boolean?,
|
||||
override val description: String?,
|
||||
override val calendarName: String?,
|
||||
override val labelOverride: String? = null,
|
||||
): CalendarEvent {
|
||||
|
||||
override val domain: String = Domain
|
||||
|
||||
override val key: String = "$domain://$id"
|
||||
|
||||
override val location: String? = null
|
||||
override val attendees: List<String> = emptyList()
|
||||
|
||||
|
||||
override fun overrideLabel(label: String): TasksCalendarEvent {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override fun launch(context: Context, options: Bundle?): Boolean {
|
||||
val uri = ContentUris.withAppendedId("content://org.tasks/tasks".toUri(), id)
|
||||
val intent = Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
return context.tryStartActivity(intent, options)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return TasksCalendarEventSerializer()
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val Domain = "tasks.org"
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package de.mm20.launcher2.calendar.providers
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.database.getIntOrNull
|
||||
import androidx.core.database.getLongOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import androidx.core.net.toUri
|
||||
import de.mm20.launcher2.search.CalendarEvent
|
||||
import de.mm20.launcher2.search.calendar.CalendarListType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class TasksCalendarProvider(
|
||||
private val context: Context,
|
||||
) : CalendarProvider {
|
||||
override suspend fun search(
|
||||
query: String?,
|
||||
from: Long,
|
||||
to: Long,
|
||||
excludedCalendars: List<String>,
|
||||
excludeAllDayEvents: Boolean,
|
||||
allowNetwork: Boolean
|
||||
): List<CalendarEvent> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
queryTasks(
|
||||
selection = buildList {
|
||||
add("dueDate >= $from")
|
||||
add("dueDate <= $to")
|
||||
if (excludedCalendars.isNotEmpty()) {
|
||||
add("cdl_id NOT IN (${excludedCalendars.joinToString()})")
|
||||
}
|
||||
if (query != null) {
|
||||
add(
|
||||
"title LIKE '%${
|
||||
query.replace("'", "").replace("%", "")
|
||||
}%'"
|
||||
)
|
||||
}
|
||||
}.joinToString(" AND ").takeIf { it.isNotEmpty() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getCalendarLists(): List<CalendarList> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val uri = "content://org.tasks/lists".toUri()
|
||||
val cursor = context.contentResolver.query(uri, arrayOf(), null, arrayOf(), null)
|
||||
?: return@withContext emptyList()
|
||||
val results = mutableListOf<CalendarList>()
|
||||
|
||||
val idIndex = cursor.getColumnIndex("cdl_id")
|
||||
val nameIndex = cursor.getColumnIndex("cdl_name")
|
||||
val colorIndex = cursor.getColumnIndex("cdl_color")
|
||||
val accountIndex = cursor.getColumnIndex("cdl_account")
|
||||
|
||||
cursor.use {
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLongOrNull(idIndex)?.toString() ?: continue
|
||||
results += CalendarList(
|
||||
id = "$namespace:$id",
|
||||
name = cursor.getStringOrNull(nameIndex) ?: continue,
|
||||
color = cursor.getIntOrNull(colorIndex) ?: 0,
|
||||
types = listOf(CalendarListType.Tasks),
|
||||
providerId = "tasks.org",
|
||||
owner = cursor.getStringOrNull(accountIndex)?.substringAfter(":", "")?.takeIf { it.isNotBlank() },
|
||||
)
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryTasks(
|
||||
selection: String? = null,
|
||||
selectionArgs: Array<String>? = arrayOf(),
|
||||
): List<CalendarEvent> {
|
||||
val uri = "content://org.tasks/todoagenda".toUri()
|
||||
val cursor = context.contentResolver.query(uri, arrayOf(), selection, selectionArgs, null)
|
||||
|
||||
val results = mutableListOf<CalendarEvent>()
|
||||
|
||||
cursor?.use {
|
||||
val idIndex = cursor.getColumnIndex("_id")
|
||||
val titleIndex = cursor.getColumnIndex("title")
|
||||
val dueIndex = cursor.getColumnIndex("dueDate")
|
||||
val completedIndex = cursor.getColumnIndex("completed")
|
||||
val notesIndex = cursor.getColumnIndex("notes")
|
||||
val colorIndex = cursor.getColumnIndex("cdl_color")
|
||||
val calendarNameIndex = cursor.getColumnIndex("cdl_name")
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
|
||||
val id = cursor.getLongOrNull(idIndex) ?: continue
|
||||
val dueDate = cursor.getLongOrNull(dueIndex)?.takeIf { it > 0L } ?: continue
|
||||
|
||||
results += TasksCalendarEvent(
|
||||
id = id,
|
||||
label = cursor.getStringOrNull(titleIndex) ?: continue,
|
||||
description = cursor.getStringOrNull(notesIndex),
|
||||
color = cursor.getIntOrNull(colorIndex),
|
||||
calendarName = cursor.getStringOrNull(calendarNameIndex),
|
||||
startTime = null,
|
||||
endTime = dueDate,
|
||||
allDay = dueDate % 60000 <= 0, // https://github.com/tasks/tasks/blob/13d4c029e855fd32ec91e4d4ec5f740ec506136e/data/src/commonMain/kotlin/org/tasks/data/entity/Task.kt#L345
|
||||
isCompleted = (cursor.getLongOrNull(completedIndex) ?: 0L) != 0L,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
suspend fun get(id: Long): CalendarEvent? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
queryTasks(
|
||||
selection = "_id = $id",
|
||||
).firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
override val namespace: String = "tasks.org"
|
||||
}
|
||||
Reference in New Issue
Block a user