diff --git a/app/app/src/main/AndroidManifest.xml b/app/app/src/main/AndroidManifest.xml
index 9a50ce2c..84565e04 100644
--- a/app/app/src/main/AndroidManifest.xml
+++ b/app/app/src/main/AndroidManifest.xml
@@ -30,6 +30,7 @@
+
(null)
- val showEditButton = settings.showEditButton.stateIn(viewModelScope, SharingStarted.Lazily, false)
+ val showEditButton =
+ settings.showEditButton.stateIn(viewModelScope, SharingStarted.Lazily, false)
abstract val tagsExpanded: Flow
abstract val compactTags: Flow
@@ -46,34 +56,44 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
) { (a, b) -> a as Boolean to b as FavoritesSettingsData }
.transformLatest {
- val columns = it.second.columns
- val excludeCalendar = it.first
- val includeFrequentlyUsed = it.second.frequentlyUsed
- val frequentlyUsedRows = it.second.frequentlyUsedRows
+ val columns = it.second.columns
+ val excludeCalendar = it.first
+ val includeFrequentlyUsed = it.second.frequentlyUsed
+ val frequentlyUsedRows = it.second.frequentlyUsedRows
- val pinned = favoritesService.getFavorites(
- excludeTypes = if (excludeCalendar) listOf("calendar", "tag", "plugin.calendar") else listOf("tag"),
- minPinnedLevel = PinnedLevel.AutomaticallySorted,
- limit = 10 * columns,
- )
- if (includeFrequentlyUsed) {
- emitAll(pinned.flatMapLatest { pinned ->
- favoritesService.getFavorites(
- excludeTypes = if (excludeCalendar) listOf("calendar", "tag", "plugin.calendar") else listOf("tag"),
- maxPinnedLevel = PinnedLevel.FrequentlyUsed,
- minPinnedLevel = PinnedLevel.FrequentlyUsed,
- limit = frequentlyUsedRows * columns - pinned.size % columns,
- ).map {
- pinned + it
- }
- .withCustomLabels(customAttributesRepository)
- })
- } else {
- emitAll(
- pinned.withCustomLabels(customAttributesRepository)
+ val pinned = favoritesService.getFavorites(
+ excludeTypes = if (excludeCalendar) listOf(
+ "calendar",
+ "tasks.org",
+ "tag",
+ "plugin.calendar"
+ ) else listOf("tag"),
+ minPinnedLevel = PinnedLevel.AutomaticallySorted,
+ limit = 10 * columns,
)
+ if (includeFrequentlyUsed) {
+ emitAll(pinned.flatMapLatest { pinned ->
+ favoritesService.getFavorites(
+ excludeTypes = if (excludeCalendar) listOf(
+ "calendar",
+ "tasks.org",
+ "tag",
+ "plugin.calendar"
+ ) else listOf("tag"),
+ maxPinnedLevel = PinnedLevel.FrequentlyUsed,
+ minPinnedLevel = PinnedLevel.FrequentlyUsed,
+ limit = frequentlyUsedRows * columns - pinned.size % columns,
+ ).map {
+ pinned + it
+ }
+ .withCustomLabels(customAttributesRepository)
+ })
+ } else {
+ emitAll(
+ pinned.withCustomLabels(customAttributesRepository)
+ )
+ }
}
- }
} else {
customAttributesRepository
.getItemsForTag(tag)
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt
index 3920d0ed..87f211b2 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt
@@ -554,6 +554,7 @@ fun ColumnScope.ConfigureCalendarWidget(
for (group in groups) {
val pluginName = remember(plugins, group.key) {
if (group.key == "local") context.getString(R.string.preference_calendar_calendars)
+ else if (group.key == "tasks.org") context.getString(R.string.preference_calendar_tasks)
else plugins.find { it.authority == group.key }?.label
}
if (pluginName != null) {
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt
index 53fb063d..efb42202 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/calendar/CalendarWidgetVM.kt
@@ -43,7 +43,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
val calendarEvents = mutableStateOf>(emptyList())
val pinnedCalendarEvents =
favoritesService.getFavorites(
- includeTypes = listOf("calendar", "plugin.calendar"),
+ includeTypes = listOf("calendar", "tasks.org", "plugin.calendar"),
minPinnedLevel = PinnedLevel.AutomaticallySorted,
).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val nextEvents = mutableStateOf>(emptyList())
@@ -172,7 +172,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
excludeCalendars = config.excludedCalendarIds ?: config.legacyExcludedCalendarIds?.map { "local:$it" } ?: emptyList(),
).collectLatest { events ->
searchableRepository.getKeys(
- includeTypes = listOf("calendar", "plugin.calendar"),
+ includeTypes = listOf("calendar", "tasks.org", "plugin.calendar"),
maxVisibility = VisibilityLevel.SearchOnly,
limit = 9999,
).collectLatest { hidden ->
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarProviderSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarProviderSettingsScreen.kt
index 4b645fae..4f2b5ade 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarProviderSettingsScreen.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarProviderSettingsScreen.kt
@@ -46,7 +46,7 @@ fun CalendarProviderSettingsScreen(providerId: String) {
val pluginState by viewModel.pluginState.collectAsStateWithLifecycle(null)
- val providerAvailable = providerId == "local" || pluginState != null
+ val providerAvailable = providerId == "local" || providerId == "tasks.org" || pluginState != null
PreferenceScreen(
title = pluginState?.plugin?.label ?: stringResource(R.string.preference_search_calendar)
@@ -59,9 +59,11 @@ fun CalendarProviderSettingsScreen(providerId: String) {
SwitchPreference(
title =
if (providerId == "local") stringResource(R.string.preference_search_calendar)
+ else if (providerId == "tasks.org") stringResource(R.string.preference_search_tasks)
else pluginState?.plugin?.label ?: "",
summary =
if (providerId == "local") stringResource(R.string.preference_search_local_calendar_summary)
+ else if (providerId == "tasks.org") stringResource(R.string.preference_search_tasks_summary)
else (pluginState?.state as? PluginState.Ready)?.text
?: pluginState?.plugin?.description,
value = enabled && (pluginState == null || pluginState?.state is PluginState.Ready),
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreen.kt
index dbf9567e..143eeb2c 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreen.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreen.kt
@@ -11,9 +11,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -39,7 +36,12 @@ fun CalendarSearchSettingsScreen() {
val navController = LocalNavController.current
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsState(null)
- val plugins by viewModel.availablePlugins.collectAsStateWithLifecycle(emptyList(), minActiveState = Lifecycle.State.RESUMED)
+ val hasTasksPermission by viewModel.hasTasksPermission.collectAsState(null)
+ val isTasksAppInstalled by viewModel.isTasksAppInstalled.collectAsStateWithLifecycle(false)
+ val plugins by viewModel.availablePlugins.collectAsStateWithLifecycle(
+ emptyList(),
+ minActiveState = Lifecycle.State.RESUMED
+ )
val enabledProviders by viewModel.enabledProviders.collectAsState(emptySet())
PreferenceScreen(title = stringResource(R.string.preference_search_calendar)) {
@@ -66,6 +68,29 @@ fun CalendarSearchSettingsScreen() {
navController?.navigate("settings/search/calendar/local")
}
)
+ if (isTasksAppInstalled) {
+ AnimatedVisibility(hasTasksPermission == false) {
+ MissingPermissionBanner(
+ text = stringResource(R.string.missing_permission_tasks_search_settings),
+ onClick = {
+ viewModel.requestTasksPermission(context as AppCompatActivity)
+ },
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+ PreferenceWithSwitch(
+ title = stringResource(R.string.preference_search_tasks),
+ summary = stringResource(R.string.preference_search_tasks_summary),
+ switchValue = enabledProviders.contains("tasks.org") && hasTasksPermission == true,
+ onSwitchChanged = {
+ viewModel.setProviderEnabled("tasks.org", it)
+ },
+ enabled = hasTasksPermission == true,
+ onClick = {
+ navController?.navigate("settings/search/calendar/tasks.org")
+ }
+ )
+ }
for (plugin in plugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreenVM.kt
index 3c5141cd..fed8b0e6 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreenVM.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreenVM.kt
@@ -1,7 +1,10 @@
package de.mm20.launcher2.ui.settings.calendarsearch
+import android.os.Process
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.calendar.providers.CalendarList
import de.mm20.launcher2.permissions.PermissionGroup
@@ -11,16 +14,25 @@ import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class CalendarSearchSettingsScreenVM : ViewModel(), KoinComponent {
private val settings: CalendarSearchSettings by inject()
private val calendarRepository: CalendarRepository by inject()
+ private val appRepository: AppRepository by inject()
private val pluginService: PluginService by inject()
private val permissionsManager: PermissionsManager by inject()
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
+ val hasTasksPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
+
+ val isTasksAppInstalled = appRepository.findOne("org.tasks", Process.myUserHandle())
+ .map { it != null }
+ .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
val availablePlugins = pluginService.getPluginsWithState(
type = PluginType.Calendar,
@@ -37,10 +49,8 @@ class CalendarSearchSettingsScreenVM : ViewModel(), KoinComponent {
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
}
- val calendarLists = calendarRepository.getCalendars()
-
- val excludedCalendars = settings.excludedCalendars
- fun setCalendarExcluded(calendarId: String, excluded: Boolean) {
- settings.setCalendarExcluded(calendarId, excluded)
+ fun requestTasksPermission(activity: AppCompatActivity) {
+ permissionsManager.requestPermission(activity, PermissionGroup.Tasks)
}
+
}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt
index 9914e039..78c4e02d 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt
@@ -111,7 +111,7 @@ fun SearchSettingsScreen() {
}
)
- if (hasLocationPlugins != false) {
+ if (hasContactPlugins != false) {
Preference(
title = stringResource(R.string.preference_search_contacts),
summary = stringResource(R.string.preference_search_contacts_summary),
diff --git a/core/base/src/main/java/de/mm20/launcher2/search/CalendarEvent.kt b/core/base/src/main/java/de/mm20/launcher2/search/CalendarEvent.kt
index c6e73adc..1bf78e04 100644
--- a/core/base/src/main/java/de/mm20/launcher2/search/CalendarEvent.kt
+++ b/core/base/src/main/java/de/mm20/launcher2/search/CalendarEvent.kt
@@ -1,9 +1,13 @@
package de.mm20.launcher2.search
import android.content.Context
+import android.content.Intent
+import androidx.core.net.toUri
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer
+import de.mm20.launcher2.ktx.tryStartActivity
+import java.net.URLEncoder
import java.text.SimpleDateFormat
interface CalendarEvent : SavableSearchable {
@@ -34,5 +38,19 @@ interface CalendarEvent : SavableSearchable {
)
}
- fun openLocation(context: Context) {}
+ fun openLocation(context: Context) {
+ if (location == null) return
+ context.tryStartActivity(
+ Intent(Intent.ACTION_VIEW)
+ .setData(
+ "geo:0,0?q=${
+ URLEncoder.encode(
+ location,
+ "utf8"
+ )
+ }".toUri()
+ )
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ }
}
\ No newline at end of file
diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml
index 29cd0dea..a46b1634 100644
--- a/core/i18n/src/main/res/values/strings.xml
+++ b/core/i18n/src/main/res/values/strings.xml
@@ -388,6 +388,7 @@
Call permission is required to start calls
Calendar permission is required to search calendar
+ Tasks permission is required to search tasks
This widget requires calendar permission
Can\'t find your calendars?
@@ -608,6 +609,8 @@
Calendar
Search upcoming appointments and events
Search calendars on this device
+ Tasks
+ Search tasks in the Tasks.org app
App shortcuts
Search app shortcuts
Calculator
@@ -630,6 +633,7 @@
Search %1$s\'s files
Owncloud
Calendars
+ Tasks
Hide completed tasks
Build information
More information about this build of this app
diff --git a/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt
index fed0555c..b2398a84 100644
--- a/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt
+++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt
@@ -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,
excludeAllDayEvents: Boolean,
): Flow> {
- 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> {
- val hasPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
+ val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
+ val hasTaskPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
val providers: Flow> = 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) })
}
}
diff --git a/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarSerialization.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarSerialization.kt
index 15de68e9..51c10776 100644
--- a/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarSerialization.kt
+++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarSerialization.kt
@@ -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,
diff --git a/data/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt
index 79d80bdd..49d711ed 100644
--- a/data/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt
+++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/Module.kt
@@ -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>(named()) { get() }
factory { CalendarRepositoryImpl(androidContext(), get(), get(), get()) }
factory(named(AndroidCalendarEvent.Domain)) { AndroidCalendarEventDeserializer(androidContext()) }
+ factory(named(TasksCalendarEvent.Domain)) { TasksCalendarEventDeserializer(androidContext()) }
factory(named(PluginCalendarEvent.Domain)) { PluginCalendarEventDeserializer(androidContext(), get()) }
}
\ No newline at end of file
diff --git a/data/calendar/src/main/java/de/mm20/launcher2/calendar/providers/AndroidCalendarEvent.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/providers/AndroidCalendarEvent.kt
index c8018caa..a0bb4d1b 100644
--- a/data/calendar/src/main/java/de/mm20/launcher2/calendar/providers/AndroidCalendarEvent.kt
+++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/providers/AndroidCalendarEvent.kt
@@ -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()
}
diff --git a/data/calendar/src/main/java/de/mm20/launcher2/calendar/providers/TasksCalendarEvent.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/providers/TasksCalendarEvent.kt
new file mode 100644
index 00000000..f29e8131
--- /dev/null
+++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/providers/TasksCalendarEvent.kt
@@ -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 = 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"
+ }
+}
\ No newline at end of file
diff --git a/data/calendar/src/main/java/de/mm20/launcher2/calendar/providers/TasksCalendarProvider.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/providers/TasksCalendarProvider.kt
new file mode 100644
index 00000000..9fcfc394
--- /dev/null
+++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/providers/TasksCalendarProvider.kt
@@ -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,
+ excludeAllDayEvents: Boolean,
+ allowNetwork: Boolean
+ ): List {
+ 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 {
+ 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()
+
+ 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? = arrayOf(),
+ ): List {
+ val uri = "content://org.tasks/todoagenda".toUri()
+ val cursor = context.contentResolver.query(uri, arrayOf(), selection, selectionArgs, null)
+
+ val results = mutableListOf()
+
+ 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"
+}
\ No newline at end of file