mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-10-15 15:59:18 +00:00
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:
parent
cb56132994
commit
06b4cf9477
|
@ -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()!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue