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