Refactor Tasks app detection (and settings update when tasks apps change) (#637)

* Refactoring

* Better live handling of (un)installed task apps

* Minor changes

* SettingsManager: explicitly mark possibility of null LiveData values

* Fix tests
This commit is contained in:
Ricki Hirner 2024-03-11 13:51:46 +01:00 committed by GitHub
parent cb56132994
commit 06b4cf9477
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 148 additions and 128 deletions

View file

@ -68,7 +68,7 @@ class SettingsManagerTest {
// posts value to main thread, InstantTaskExecutorRule is required to execute it instantly // posts value to main thread, InstantTaskExecutorRule is required to execute it instantly
settingsManager.putBoolean(SETTING_TEST, true) settingsManager.putBoolean(SETTING_TEST, true)
runBlocking(Dispatchers.Main) { // observeForever can't be run in background thread runBlocking(Dispatchers.Main) { // observeForever can't be run in background thread
assertTrue(live.getOrAwaitValue()) assertTrue(live.getOrAwaitValue()!!)
} }
} }

View file

@ -10,7 +10,6 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration import androidx.work.Configuration
import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.UiUtils import at.bitfire.davdroid.ui.UiUtils
@ -65,10 +64,8 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide
// watch storage because low storage means synchronization is stopped // watch storage because low storage means synchronization is stopped
storageLowReceiver.listen() storageLowReceiver.listen()
// watch installed/removed apps // watch installed/removed tasks apps and update sync settings accordingly
TasksWatcher.watch(this) TasksWatcher.watch(this)
// check whether a tasks app is currently installed
SyncUtils.updateTaskSync(this)
// create/update app shortcuts // create/update app shortcuts
UiUtils.updateShortcuts(this) UiUtils.updateShortcuts(this)
@ -87,4 +84,4 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide
exitProcess(1) exitProcess(1)
} }
} }

View file

@ -8,22 +8,39 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import androidx.annotation.MainThread
abstract class PackageChangedReceiver( abstract class PackageChangedReceiver(
val context: Context val context: Context
): BroadcastReceiver(), AutoCloseable { ): BroadcastReceiver(), AutoCloseable {
init { /**
* Registers the receiver.
*
* @param whether [onPackageChanged] shall be called immediately after registering
*/
fun register(immediateCall: Boolean = false) {
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply { val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
addAction(Intent.ACTION_PACKAGE_CHANGED) addAction(Intent.ACTION_PACKAGE_CHANGED)
addAction(Intent.ACTION_PACKAGE_REMOVED) addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package") addDataScheme("package")
} }
context.registerReceiver(this, filter) context.registerReceiver(this, filter)
if (immediateCall)
onPackageChanged()
} }
override fun close() { override fun close() {
context.unregisterReceiver(this) context.unregisterReceiver(this)
} }
@MainThread
abstract fun onPackageChanged()
override fun onReceive(context: Context, intent: Intent) {
onPackageChanged()
}
} }

View file

@ -5,26 +5,55 @@
package at.bitfire.davdroid package at.bitfire.davdroid
import android.content.Context import android.content.Context
import android.content.Intent import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.syncadapter.SyncUtils.updateTaskSync import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.ical4android.TaskProvider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/**
* Watches whether a tasks app has been installed or uninstalled and updates
* the selected tasks app and task sync settings accordingly.
*/
class TasksWatcher private constructor( class TasksWatcher private constructor(
context: Context context: Context
): PackageChangedReceiver(context) { ): PackageChangedReceiver(context) {
companion object { companion object {
fun watch(context: Context) = TasksWatcher(context) fun watch(context: Context) {
TasksWatcher(context).register(true)
}
} }
override fun onReceive(context: Context, intent: Intent) { override fun onPackageChanged() {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
updateTaskSync(context) if (TaskUtils.currentProvider(context) == null) {
/* Currently no usable tasks provider.
Iterate through all supported providers and select one, if available. */
var providerSelected = false
for (provider in TaskProvider.ProviderName.entries) {
val available = context.packageManager.resolveContentProvider(provider.authority, 0) != null
if (available) {
Logger.log.info("Selecting new tasks provider: $provider")
TaskUtils.selectProvider(context, provider, updateSyncSettings = false)
providerSelected = true
break
}
}
if (!providerSelected)
// no provider available, also clear setting
TaskUtils.selectProvider(context, null, updateSyncSettings = false)
}
// update sync settings
SyncUtils.updateTaskSync(context)
} }
} }
} }

View file

@ -16,9 +16,6 @@ import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object TaskUtils { object TaskUtils {
@ -28,16 +25,29 @@ object TaskUtils {
fun settingsManager(): SettingsManager fun settingsManager(): SettingsManager
} }
/**
* Returns the currently selected tasks provider (if it's still available = installed).
*
* @return the currently selected tasks provider, or null if none is available
*/
fun currentProvider(context: Context): ProviderName? { fun currentProvider(context: Context): ProviderName? {
val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager() val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager()
val preferredAuthority = settingsManager.getString(Settings.PREFERRED_TASKS_PROVIDER) ?: return null val preferredAuthority = settingsManager.getString(Settings.SELECTED_TASKS_PROVIDER) ?: return null
return preferredAuthorityToProviderName(preferredAuthority, context.packageManager) return preferredAuthorityToProviderName(preferredAuthority, context.packageManager)
} }
/**
* Returns the currently selected tasks provider (if it's still available = installed).
*
* @return the currently selected tasks provider, or null if none is available
*/
fun currentProviderLive(context: Context): LiveData<ProviderName?> { fun currentProviderLive(context: Context): LiveData<ProviderName?> {
val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager() val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager()
return settingsManager.getStringLive(Settings.PREFERRED_TASKS_PROVIDER).map { preferred -> return settingsManager.getStringLive(Settings.SELECTED_TASKS_PROVIDER).map { preferred ->
preferredAuthorityToProviderName(preferred, context.packageManager) if (preferred != null)
preferredAuthorityToProviderName(preferred, context.packageManager)
else
null
} }
} }
@ -56,12 +66,12 @@ object TaskUtils {
fun isAvailable(context: Context) = currentProvider(context) != null fun isAvailable(context: Context) = currentProvider(context) != null
fun setPreferredProvider(context: Context, providerName: ProviderName) { fun selectProvider(context: Context, providerName: ProviderName?, updateSyncSettings: Boolean = false) {
val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager() val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager()
settingsManager.putString(Settings.PREFERRED_TASKS_PROVIDER, providerName.authority) settingsManager.putString(Settings.SELECTED_TASKS_PROVIDER, providerName?.authority)
CoroutineScope(Dispatchers.Default).launch {
SyncUtils.updateTaskSync(context) // update sync settings
} SyncUtils.updateTaskSync(context)
} }
} }

View file

@ -39,10 +39,13 @@ object Settings {
const val PREFERRED_THEME = "preferred_theme" const val PREFERRED_THEME = "preferred_theme"
const val PREFERRED_THEME_DEFAULT = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM const val PREFERRED_THEME_DEFAULT = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
const val LANGUAGE = "language" /**
const val LANGUAGE_SYSTEM = "language_system" * Selected tasks app. When at least one tasks app is installed, this setting is set to its authority.
* In case of multiple available tasks app, the user can choose one and this setting will reflect the selected one.
const val PREFERRED_TASKS_PROVIDER = "preferred_tasks_provider" *
* If no tasks app is installed, this setting is not set.
*/
const val SELECTED_TASKS_PROVIDER = "preferred_tasks_provider"
/** whether collections are automatically selected for synchronization after their initial detection */ /** whether collections are automatically selected for synchronization after their initial detection */
const val PRESELECT_COLLECTIONS = "preselect_collections" const val PRESELECT_COLLECTIONS = "preselect_collections"

View file

@ -125,17 +125,17 @@ class SettingsManager internal constructor(
fun getBooleanOrNull(key: String): Boolean? = getValue(key) { provider -> provider.getBoolean(key) } fun getBooleanOrNull(key: String): Boolean? = getValue(key) { provider -> provider.getBoolean(key) }
fun getBoolean(key: String): Boolean = getBooleanOrNull(key) ?: throw NoSuchPropertyException(key) fun getBoolean(key: String): Boolean = getBooleanOrNull(key) ?: throw NoSuchPropertyException(key)
fun getBooleanLive(key: String): LiveData<Boolean> = SettingLiveData { getBooleanOrNull(key) } fun getBooleanLive(key: String): LiveData<Boolean?> = SettingLiveData { getBooleanOrNull(key) }
fun getIntOrNull(key: String): Int? = getValue(key) { provider -> provider.getInt(key) } fun getIntOrNull(key: String): Int? = getValue(key) { provider -> provider.getInt(key) }
fun getInt(key: String): Int = getIntOrNull(key) ?: throw NoSuchPropertyException(key) fun getInt(key: String): Int = getIntOrNull(key) ?: throw NoSuchPropertyException(key)
fun getIntLive(key: String): LiveData<Int> = SettingLiveData { getIntOrNull(key) } fun getIntLive(key: String): LiveData<Int?> = SettingLiveData { getIntOrNull(key) }
fun getLongOrNull(key: String): Long? = getValue(key) { provider -> provider.getLong(key) } fun getLongOrNull(key: String): Long? = getValue(key) { provider -> provider.getLong(key) }
fun getLong(key: String) = getLongOrNull(key) ?: throw NoSuchPropertyException(key) fun getLong(key: String) = getLongOrNull(key) ?: throw NoSuchPropertyException(key)
fun getString(key: String) = getValue(key) { provider -> provider.getString(key) } fun getString(key: String) = getValue(key) { provider -> provider.getString(key) }
fun getStringLive(key: String): LiveData<String> = SettingLiveData { getString(key) } fun getStringLive(key: String): LiveData<String?> = SettingLiveData { getString(key) }
fun isWritable(key: String): Boolean { fun isWritable(key: String): Boolean {

View file

@ -114,15 +114,17 @@ object SyncUtils {
// task sync utils // task sync utils
/**
* Sets up sync for the current TaskProvider (and disables sync for unavailable task providers).
*
* In case of missing permissions, a notification is shown.
*/
@WorkerThread @WorkerThread
fun updateTaskSync(context: Context) { fun updateTaskSync(context: Context) {
val tasksProvider = TaskUtils.currentProvider(context) val currentProvider = TaskUtils.currentProvider(context)
Logger.log.info("App launched or other package (un)installed; current tasks provider = $tasksProvider") Logger.log.info("App launched or other package (un)installed; current tasks provider = $currentProvider")
var permissionsRequired = false // whether additional permissions are required var permissionsRequired = false // whether additional permissions are required
val currentProvider by lazy { // only this provider shall be enabled (null to disable all providers)
TaskUtils.currentProvider(context)
}
// check all accounts and (de)activate task provider(s) if a CalDAV service is defined // check all accounts and (de)activate task provider(s) if a CalDAV service is defined
val db = EntryPointAccessors.fromApplication(context, SyncUtilsEntryPoint::class.java).appDatabase() val db = EntryPointAccessors.fromApplication(context, SyncUtilsEntryPoint::class.java).appDatabase()

View file

@ -151,7 +151,7 @@ class AppSettingsActivity: AppCompatActivity() {
) )
AppSettings_Connection( AppSettings_Connection(
proxyType = model.settings.getIntLive(Settings.PROXY_TYPE).observeAsState(Settings.PROXY_TYPE_NONE).value, proxyType = model.settings.getIntLive(Settings.PROXY_TYPE).observeAsState().value ?: Settings.PROXY_TYPE_NONE,
onProxyTypeUpdated = { model.settings.putInt(Settings.PROXY_TYPE, it) }, onProxyTypeUpdated = { model.settings.putInt(Settings.PROXY_TYPE, it) },
proxyHostName = model.settings.getStringLive(Settings.PROXY_HOST).observeAsState(null).value, proxyHostName = model.settings.getStringLive(Settings.PROXY_HOST).observeAsState(null).value,
onProxyHostNameUpdated = { model.settings.putString(Settings.PROXY_HOST, it) }, onProxyHostNameUpdated = { model.settings.putString(Settings.PROXY_HOST, it) },
@ -160,7 +160,7 @@ class AppSettingsActivity: AppCompatActivity() {
) )
AppSettings_Security( AppSettings_Security(
distrustSystemCerts = model.settings.getBooleanLive(Settings.DISTRUST_SYSTEM_CERTIFICATES).observeAsState(false).value, distrustSystemCerts = model.settings.getBooleanLive(Settings.DISTRUST_SYSTEM_CERTIFICATES).observeAsState().value ?: false,
onDistrustSystemCertsUpdated = { model.settings.putBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES, it) }, onDistrustSystemCertsUpdated = { model.settings.putBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES, it) },
onResetCertificates = { onResetCertificates = {
model.resetCertificates() model.resetCertificates()
@ -172,7 +172,7 @@ class AppSettingsActivity: AppCompatActivity() {
) )
AppSettings_UserInterface( AppSettings_UserInterface(
theme = model.settings.getIntLive(Settings.PREFERRED_THEME).observeAsState(Settings.PREFERRED_THEME_DEFAULT).value, theme = model.settings.getIntLive(Settings.PREFERRED_THEME).observeAsState().value ?: Settings.PREFERRED_THEME_DEFAULT,
onThemeSelected = { onThemeSelected = {
model.settings.putInt(Settings.PREFERRED_THEME, it) model.settings.putInt(Settings.PREFERRED_THEME, it)
UiUtils.updateTheme(context) UiUtils.updateTheme(context)

View file

@ -6,7 +6,6 @@ package at.bitfire.davdroid.ui
import android.Manifest import android.Manifest
import android.app.Application import android.app.Application
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -50,7 +49,6 @@ import at.bitfire.davdroid.util.PermissionUtils
import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.themeadapter.material.MdcTheme
import java.util.logging.Level import java.util.logging.Level
class PermissionsActivity: AppCompatActivity() { class PermissionsActivity: AppCompatActivity() {
@ -82,13 +80,13 @@ class PermissionsActivity: AppCompatActivity() {
val jtxAvailable = MutableLiveData<Boolean>() val jtxAvailable = MutableLiveData<Boolean>()
private val tasksWatcher = object: PackageChangedReceiver(app) { private val tasksWatcher = object: PackageChangedReceiver(app) {
@MainThread override fun onPackageChanged() {
override fun onReceive(context: Context?, intent: Intent?) {
checkPermissions() checkPermissions()
} }
} }
init { init {
tasksWatcher.register()
checkPermissions() checkPermissions()
} }

View file

@ -5,14 +5,12 @@
package at.bitfire.davdroid.ui package at.bitfire.davdroid.ui
import android.app.Application import android.app.Application
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.AnyThread
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -45,9 +43,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.PackageChangedReceiver import at.bitfire.davdroid.PackageChangedReceiver
@ -60,6 +59,7 @@ import at.bitfire.davdroid.ui.widget.RadioWithSwitch
import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -80,9 +80,9 @@ class TasksActivity: AppCompatActivity() {
@HiltViewModel @HiltViewModel
class Model @Inject constructor( class Model @Inject constructor(
application: Application, val context: Application,
val settings: SettingsManager val settings: SettingsManager
) : AndroidViewModel(application), SettingsManager.OnChangeListener { ) : ViewModel() {
companion object { companion object {
@ -95,83 +95,46 @@ class TasksActivity: AppCompatActivity() {
} }
val currentProvider = MutableLiveData<TaskProvider.ProviderName>() val dontShow = settings.getBooleanLive(HINT_OPENTASKS_NOT_INSTALLED)
val openTasksInstalled = MutableLiveData<Boolean>() fun setDontShow(dontShow: Boolean) {
val openTasksRequested = MutableLiveData<Boolean>() settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, !dontShow)
val openTasksSelected = MutableLiveData<Boolean>() }
val tasksOrgInstalled = MutableLiveData<Boolean>()
val tasksOrgRequested = MutableLiveData<Boolean>()
val tasksOrgSelected = MutableLiveData<Boolean>()
val jtxInstalled = MutableLiveData<Boolean>()
val jtxRequested = MutableLiveData<Boolean>()
val jtxSelected = MutableLiveData<Boolean>()
private val tasksWatcher = object: PackageChangedReceiver(application) { val currentProvider = TaskUtils.currentProviderLive(context)
override fun onReceive(context: Context?, intent: Intent?) { val jtxSelected = currentProvider.map { it == TaskProvider.ProviderName.JtxBoard }
checkInstalled() val tasksOrgSelected = currentProvider.map { it == TaskProvider.ProviderName.TasksOrg }
val openTasksSelected = currentProvider.map { it == TaskProvider.ProviderName.OpenTasks }
val jtxInstalled = MutableLiveData<Boolean>()
val tasksOrgInstalled = MutableLiveData<Boolean>()
val openTasksInstalled = MutableLiveData<Boolean>()
private val pkgChangedReceiver = object: PackageChangedReceiver(context) {
override fun onPackageChanged() {
jtxInstalled.postValue(isInstalled(TaskProvider.ProviderName.JtxBoard.packageName))
tasksOrgInstalled.postValue(isInstalled(TaskProvider.ProviderName.TasksOrg.packageName))
openTasksInstalled.postValue(isInstalled(TaskProvider.ProviderName.OpenTasks.packageName))
} }
} }
val dontShow = MutableLiveData(
settings.getBooleanOrNull(HINT_OPENTASKS_NOT_INSTALLED) == false
)
private val dontShowObserver = Observer<Boolean> { value ->
if (value)
settings.putBoolean(HINT_OPENTASKS_NOT_INSTALLED, false)
else
settings.remove(HINT_OPENTASKS_NOT_INSTALLED)
}
init { init {
checkInstalled() pkgChangedReceiver.register(true)
settings.addOnChangeListener(this)
dontShow.observeForever(dontShowObserver)
} }
override fun onCleared() { override fun onCleared() {
settings.removeOnChangeListener(this) pkgChangedReceiver.close()
tasksWatcher.close()
dontShow.removeObserver(dontShowObserver)
}
@AnyThread
fun checkInstalled() {
val taskProvider = TaskUtils.currentProvider(getApplication())
currentProvider.postValue(taskProvider)
val openTasks = isInstalled(TaskProvider.ProviderName.OpenTasks.packageName)
openTasksInstalled.postValue(openTasks)
openTasksRequested.postValue(openTasks)
openTasksSelected.postValue(taskProvider == TaskProvider.ProviderName.OpenTasks)
val tasksOrg = isInstalled(TaskProvider.ProviderName.TasksOrg.packageName)
tasksOrgInstalled.postValue(tasksOrg)
tasksOrgRequested.postValue(tasksOrg)
tasksOrgSelected.postValue(taskProvider == TaskProvider.ProviderName.TasksOrg)
val jtxBoard = isInstalled(TaskProvider.ProviderName.JtxBoard.packageName)
jtxInstalled.postValue(jtxBoard)
jtxRequested.postValue(jtxBoard)
jtxSelected.postValue(taskProvider == TaskProvider.ProviderName.JtxBoard)
} }
private fun isInstalled(packageName: String): Boolean = private fun isInstalled(packageName: String): Boolean =
try { try {
getApplication<Application>().packageManager.getPackageInfo(packageName, 0) context.packageManager.getPackageInfo(packageName, 0)
true true
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
false false
} }
fun selectPreferredProvider(provider: TaskProvider.ProviderName) { fun selectProvider(provider: TaskProvider.ProviderName) = viewModelScope.launch(Dispatchers.Default) {
// Changes preferred task app setting, so onSettingsChanged() will be called TaskUtils.selectProvider(context, provider)
TaskUtils.setPreferredProvider(getApplication(), provider)
}
override fun onSettingsChanged() {
checkInstalled()
} }
} }
@ -190,19 +153,16 @@ fun TasksCard(
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val jtxInstalled by model.jtxInstalled.observeAsState(initial = false) val jtxInstalled by model.jtxInstalled.observeAsState(false)
val jtxSelected by model.jtxSelected.observeAsState(initial = false) val jtxSelected by model.jtxSelected.observeAsState(false)
val jtxRequested by model.jtxRequested.observeAsState(initial = false)
val tasksOrgInstalled by model.tasksOrgInstalled.observeAsState(initial = false) val tasksOrgInstalled by model.tasksOrgInstalled.observeAsState(false)
val tasksOrgSelected by model.tasksOrgSelected.observeAsState(initial = false) val tasksOrgSelected by model.tasksOrgSelected.observeAsState(false)
val tasksOrgRequested by model.tasksOrgRequested.observeAsState(initial = false)
val openTasksInstalled by model.openTasksInstalled.observeAsState(initial = false) val openTasksInstalled by model.openTasksInstalled.observeAsState(false)
val openTasksSelected by model.openTasksSelected.observeAsState(initial = false) val openTasksSelected by model.openTasksSelected.observeAsState(false)
val openTasksRequested by model.openTasksRequested.observeAsState(initial = false)
val dontShow by model.dontShow.observeAsState(initial = false) val dontShow = model.dontShow.observeAsState().value ?: false
fun installApp(packageName: String) { fun installApp(packageName: String) {
val uri = Uri.parse("market://details?id=$packageName&referrer=" + val uri = Uri.parse("market://details?id=$packageName&referrer=" +
@ -221,7 +181,7 @@ fun TasksCard(
fun onProviderSelected(provider: TaskProvider.ProviderName) { fun onProviderSelected(provider: TaskProvider.ProviderName) {
if (model.currentProvider.value != provider) if (model.currentProvider.value != provider)
model.selectPreferredProvider(provider) model.selectProvider(provider)
} }
Scaffold( Scaffold(
@ -249,7 +209,7 @@ fun TasksCard(
Text(stringResource(R.string.intro_tasks_jtx_info)) Text(stringResource(R.string.intro_tasks_jtx_info))
}, },
isSelected = jtxSelected, isSelected = jtxSelected,
isToggled = jtxRequested, isToggled = jtxInstalled,
enabled = jtxInstalled, enabled = jtxInstalled,
onSelected = { onProviderSelected(TaskProvider.ProviderName.JtxBoard) }, onSelected = { onProviderSelected(TaskProvider.ProviderName.JtxBoard) },
onToggled = { toggled -> onToggled = { toggled ->
@ -280,7 +240,7 @@ fun TasksCard(
) )
}, },
isSelected = tasksOrgSelected, isSelected = tasksOrgSelected,
isToggled = tasksOrgRequested, isToggled = tasksOrgInstalled,
enabled = tasksOrgInstalled, enabled = tasksOrgInstalled,
onSelected = { onProviderSelected(TaskProvider.ProviderName.TasksOrg) }, onSelected = { onProviderSelected(TaskProvider.ProviderName.TasksOrg) },
onToggled = { toggled -> onToggled = { toggled ->
@ -297,7 +257,7 @@ fun TasksCard(
Text(stringResource(R.string.intro_tasks_opentasks_info)) Text(stringResource(R.string.intro_tasks_opentasks_info))
}, },
isSelected = openTasksSelected, isSelected = openTasksSelected,
isToggled = openTasksRequested, isToggled = openTasksInstalled,
enabled = openTasksInstalled, enabled = openTasksInstalled,
onSelected = { onProviderSelected(TaskProvider.ProviderName.OpenTasks) }, onSelected = { onProviderSelected(TaskProvider.ProviderName.OpenTasks) },
onToggled = { toggled -> onToggled = { toggled ->
@ -316,13 +276,13 @@ fun TasksCard(
) { ) {
Checkbox( Checkbox(
checked = dontShow, checked = dontShow,
onCheckedChange = { model.dontShow.value = it } onCheckedChange = { model.setDontShow(it) }
) )
Text( Text(
text = stringResource(R.string.intro_tasks_dont_show), text = stringResource(R.string.intro_tasks_dont_show),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { model.dontShow.value = !dontShow } .clickable { model.setDontShow(!dontShow) }
) )
} }
} }

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@ -121,7 +122,8 @@ class OpenSourcePage : IntroPage {
Text(stringResource(R.string.intro_open_source_details)) Text(stringResource(R.string.intro_open_source_details))
} }
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) { ) {
Checkbox( Checkbox(
checked = dontShow, checked = dontShow,
@ -130,7 +132,9 @@ class OpenSourcePage : IntroPage {
Text( Text(
text = stringResource(R.string.intro_open_source_dont_show), text = stringResource(R.string.intro_open_source_dont_show),
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body2,
modifier = Modifier.clickable { onChangeDontShow(!dontShow) } modifier = Modifier
.clickable { onChangeDontShow(!dontShow) }
.weight(1f)
) )
} }
} }