diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt
index e058b85e..8d0c1ae9 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt
@@ -69,6 +69,7 @@ import de.mm20.launcher2.ui.settings.plugins.PluginsSettingsScreen
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen
import de.mm20.launcher2.ui.settings.tags.TagsSettingsScreen
+import de.mm20.launcher2.ui.settings.tasks.TasksIntegrationSettingsScreen
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterHelpSettingsScreen
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterSettingsScreen
import de.mm20.launcher2.ui.settings.weather.WeatherIntegrationSettingsScreen
@@ -240,6 +241,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/integrations/owncloud") {
OwncloudSettingsScreen()
}
+ composable("settings/integrations/tasks") {
+ TasksIntegrationSettingsScreen()
+ }
composable("settings/plugins") {
PluginsSettingsScreen()
}
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt
index e86ed114..1961f160 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt
@@ -1,6 +1,7 @@
package de.mm20.launcher2.ui.settings.integrations
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.PlayCircleOutline
import androidx.compose.runtime.Composable
@@ -49,6 +50,13 @@ fun IntegrationsSettingsScreen() {
navController?.navigate("settings/integrations/owncloud")
}
)
+ Preference(
+ title = stringResource(R.string.preference_tasks_integration),
+ icon = Icons.Default.Check,
+ onClick = {
+ navController?.navigate("settings/integrations/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 78c4e02d..d05a87f5 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
@@ -63,7 +63,7 @@ fun SearchSettingsScreen() {
val hasCalendarPlugins by remember { derivedStateOf { plugins?.any { it.plugin.type == PluginType.Calendar } } }
val hasLocationPlugins by remember { derivedStateOf { plugins?.any { it.plugin.type == PluginType.LocationSearch } } }
val hasContactPlugins by remember { derivedStateOf { plugins?.any { it.plugin.type == PluginType.ContactSearch } } }
-
+ val isTasksAppInstalled by viewModel.isTasksAppInstalled.collectAsStateWithLifecycle()
val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(null)
val hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(null)
@@ -145,7 +145,7 @@ fun SearchSettingsScreen() {
)
}
- if (hasCalendarPlugins != false) {
+ if (hasCalendarPlugins != false || isTasksAppInstalled != false) {
Preference(
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary),
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt
index 0c6bf587..b790cfdd 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt
@@ -1,8 +1,10 @@
package de.mm20.launcher2.ui.settings.search
+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.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.plugins.PluginService
@@ -18,6 +20,7 @@ import de.mm20.launcher2.preferences.search.WikipediaSearchSettings
import de.mm20.launcher2.preferences.ui.SearchUiSettings
import de.mm20.launcher2.search.SearchFilters
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -34,6 +37,8 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
private val locationSearchSettings: LocationSearchSettings by inject()
private val searchFilterSettings: SearchFilterSettings by inject()
+ private val appRepository: AppRepository by inject()
+
private val pluginService: PluginService by inject()
private val permissionsManager: PermissionsManager by inject()
@@ -159,4 +164,8 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
val plugins = pluginService.getPluginsWithState(enabled = true)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
+
+ val isTasksAppInstalled = appRepository.findOne("org.tasks", Process.myUserHandle())
+ .map { it != null }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tasks/TasksSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tasks/TasksSettingsScreen.kt
new file mode 100644
index 00000000..fed0938d
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tasks/TasksSettingsScreen.kt
@@ -0,0 +1,104 @@
+package de.mm20.launcher2.ui.settings.tasks
+
+import androidx.activity.compose.LocalActivity
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.OpenInNew
+import androidx.compose.material.icons.rounded.CheckCircle
+import androidx.compose.material.icons.rounded.Info
+import androidx.compose.material.icons.rounded.TaskAlt
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+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.Preference
+import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
+import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
+import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
+import de.mm20.launcher2.ui.locals.LocalNavController
+
+@Composable
+fun TasksIntegrationSettingsScreen() {
+ val viewModel: TasksSettingsScreenVM = viewModel()
+ val activity = LocalActivity.current
+ val navController = LocalNavController.current
+
+ val isTasksInstalled by viewModel.isTasksAppInstalled.collectAsStateWithLifecycle(null)
+ val hasTasksPermission by viewModel.hasTasksPermission.collectAsStateWithLifecycle(null)
+ val isTasksSearchEnabled by viewModel.isTasksSearchEnabled.collectAsStateWithLifecycle(false)
+
+ PreferenceScreen(
+ title = stringResource(R.string.preference_tasks_integration)
+ ) {
+ if (isTasksInstalled == false) {
+ item {
+ Banner(
+ text = stringResource(
+ R.string.preference_tasks_integration_description,
+ stringResource(R.string.app_name)
+ ),
+ icon = Icons.Rounded.Info,
+ modifier = Modifier.padding(16.dp),
+ primaryAction = {
+ Button(onClick = {
+ viewModel.downloadTasksApp(activity as AppCompatActivity)
+ }) {
+ Text(stringResource(R.string.action_install))
+ }
+ }
+ )
+ }
+ }
+ if (isTasksInstalled == true) {
+ item {
+ PreferenceCategory {
+ if (hasTasksPermission == false) {
+ MissingPermissionBanner(
+ modifier = Modifier.padding(16.dp),
+ text = stringResource(R.string.missing_permission_tasks_integration),
+ onClick = {
+ viewModel.requestTasksPermission(activity as AppCompatActivity)
+ }
+ )
+ }
+ if (hasTasksPermission == true) {
+ Banner(
+ text = stringResource(R.string.preference_tasks_integration_ready),
+ icon = Icons.Rounded.CheckCircle,
+ modifier = Modifier.padding(16.dp),
+ )
+ }
+ PreferenceWithSwitch(
+ icon = Icons.Rounded.TaskAlt,
+ title = stringResource(R.string.preference_search_tasks),
+ summary = stringResource(R.string.preference_search_tasks_summary),
+ switchValue = isTasksSearchEnabled == true && hasTasksPermission == true,
+ onSwitchChanged = {
+ viewModel.setTasksSearchEnabled(it)
+ },
+ enabled = hasTasksPermission == true,
+ onClick = {
+ navController?.navigate("settings/search/calendar/tasks.org")
+ }
+ )
+ Preference(
+ title = "Open Tasks app",
+ icon = Icons.AutoMirrored.Rounded.OpenInNew,
+ onClick = {
+ viewModel.launchTasksApp(activity as AppCompatActivity)
+ }
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tasks/TasksSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tasks/TasksSettingsScreenVM.kt
new file mode 100644
index 00000000..8a0104c9
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tasks/TasksSettingsScreenVM.kt
@@ -0,0 +1,64 @@
+package de.mm20.launcher2.ui.settings.tasks
+
+import android.content.Intent
+import android.os.Process
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.net.toUri
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import de.mm20.launcher2.applications.AppRepository
+import de.mm20.launcher2.icons.IconService
+import de.mm20.launcher2.permissions.PermissionGroup
+import de.mm20.launcher2.permissions.PermissionsManager
+import de.mm20.launcher2.preferences.search.CalendarSearchSettings
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class TasksSettingsScreenVM : ViewModel(), KoinComponent {
+ private val appRepository: AppRepository by inject()
+ private val permissionsManager: PermissionsManager by inject()
+ private val iconService: IconService by inject()
+ private val calendarSearchSettings: CalendarSearchSettings by inject()
+
+ val tasksApp = appRepository.findOne("org.tasks", Process.myUserHandle())
+ .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
+
+ val isTasksAppInstalled = tasksApp
+ .map { it != null }
+ .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
+
+ val hasTasksPermission = permissionsManager.hasPermission(PermissionGroup.Tasks)
+ .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
+
+ fun requestTasksPermission(activity: AppCompatActivity) {
+ permissionsManager.requestPermission(activity, PermissionGroup.Tasks)
+ }
+
+ fun downloadTasksApp(activity: AppCompatActivity) {
+ activity.startActivity(
+ Intent(Intent.ACTION_VIEW).apply {
+ data = "https://tasks.org/".toUri()
+ }
+ )
+ }
+
+ fun launchTasksApp(activity: AppCompatActivity) {
+ viewModelScope.launch {
+ tasksApp.first()?.launch(activity, null)
+ }
+ }
+
+ val isTasksSearchEnabled = calendarSearchSettings.isProviderEnabled("tasks.org")
+ .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
+
+ fun setTasksSearchEnabled(enabled: Boolean) {
+ calendarSearchSettings.setProviderEnabled("tasks.org", enabled)
+ }
+}
\ 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 a46b1634..7203735c 100644
--- a/core/i18n/src/main/res/values/strings.xml
+++ b/core/i18n/src/main/res/values/strings.xml
@@ -37,6 +37,7 @@
Done
More actions
Clear
+ Install
Delete
Remove
@@ -389,6 +390,7 @@
Calendar permission is required to search calendar
Tasks permission is required to search tasks
+ Tasks permission is required to use the Tasks integration
This widget requires calendar permission
Can\'t find your calendars?
@@ -610,7 +612,7 @@
Search upcoming appointments and events
Search calendars on this device
Tasks
- Search tasks in the Tasks.org app
+ Search tasks in the Tasks app
App shortcuts
Search app shortcuts
Calculator
@@ -665,6 +667,10 @@
Grid, icon size, icon packs, badges
Weather
Media control
+ Tasks
+
+ Tasks is a free and open source to-do list and reminders app. If installed, %1$s can display and search your tasks from the Tasks app.
+ Tasks integration is fully set up and ready to use.
Tap to call
Immediately start a call when tapping a phone number