diff --git a/app/src/main/kotlin/at/bitfire/davdroid/App.kt b/app/src/main/kotlin/at/bitfire/davdroid/App.kt index b6780687..0b7fa7a8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/App.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/App.kt @@ -61,7 +61,7 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide // watch for account changes/deletions accountsUpdatedListener.listen() - // watch storage because low storage means synchronization is stopped + // watch storage because low storage means sync framework stops local content update notifications storageLowReceiver.listen() // watch installed/removed tasks apps and update sync settings accordingly diff --git a/app/src/main/kotlin/at/bitfire/davdroid/TasksWatcher.kt b/app/src/main/kotlin/at/bitfire/davdroid/TasksWatcher.kt index 2c9ff83e..4d0e640b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/TasksWatcher.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/TasksWatcher.kt @@ -6,8 +6,7 @@ package at.bitfire.davdroid import android.content.Context import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.TaskUtils -import at.bitfire.davdroid.syncadapter.SyncUtils +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.ical4android.TaskProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -31,28 +30,26 @@ class TasksWatcher private constructor( override fun onPackageChanged() { CoroutineScope(Dispatchers.Default).launch { - if (TaskUtils.currentProvider(context) == null) { - /* Currently no usable tasks provider. - Iterate through all supported providers and select one, if available. */ + val currentProvider = TaskUtils.currentProvider(context) + Logger.log.info("App launched or package (un)installed; current tasks provider = $currentProvider") + if (currentProvider == null) { + // 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) + TaskUtils.selectProvider(context, provider) providerSelected = true break } } if (!providerSelected) - // no provider available, also clear setting - TaskUtils.selectProvider(context, null, updateSyncSettings = false) + // no provider available (anymore), also clear setting and sync + TaskUtils.selectProvider(context, null) } - - // update sync settings - SyncUtils.updateTaskSync(context) } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/TaskUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/TaskUtils.kt deleted file mode 100644 index 7adb3219..00000000 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/TaskUtils.kt +++ /dev/null @@ -1,77 +0,0 @@ -/*************************************************************************************************** - * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. - **************************************************************************************************/ - -package at.bitfire.davdroid.resource - -import android.content.Context -import android.content.pm.PackageManager -import androidx.lifecycle.LiveData -import androidx.lifecycle.map -import at.bitfire.davdroid.settings.Settings -import at.bitfire.davdroid.settings.SettingsManager -import at.bitfire.davdroid.syncadapter.SyncUtils -import at.bitfire.ical4android.TaskProvider.ProviderName -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent - -object TaskUtils { - - @EntryPoint - @InstallIn(SingletonComponent::class) - interface TaskUtilsEntryPoint { - 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? { - val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager() - val preferredAuthority = settingsManager.getString(Settings.SELECTED_TASKS_PROVIDER) ?: return null - 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 { - val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager() - return settingsManager.getStringLive(Settings.SELECTED_TASKS_PROVIDER).map { preferred -> - if (preferred != null) - preferredAuthorityToProviderName(preferred, context.packageManager) - else - null - } - } - - private fun preferredAuthorityToProviderName( - preferredAuthority: String, - packageManager: PackageManager - ): ProviderName? { - ProviderName.entries.toTypedArray() - .sortedByDescending { it.authority == preferredAuthority } - .forEach { providerName -> - if (packageManager.resolveContentProvider(providerName.authority, 0) != null) - return providerName - } - return null - } - - fun isAvailable(context: Context) = currentProvider(context) != null - - fun selectProvider(context: Context, providerName: ProviderName?, updateSyncSettings: Boolean = false) { - val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager() - settingsManager.putString(Settings.SELECTED_TASKS_PROVIDER, providerName?.authority) - - if (updateSyncSettings) - SyncUtils.updateTaskSync(context) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt index d4cad617..fe3fc02e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt @@ -22,7 +22,7 @@ import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.resource.LocalTask -import at.bitfire.davdroid.resource.TaskUtils +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.syncadapter.BaseSyncWorker import at.bitfire.davdroid.syncadapter.SyncUtils import at.bitfire.davdroid.util.setAndVerifyUserData @@ -420,7 +420,7 @@ class AccountSettingsMigrations( @Suppress("unused") private fun update_4_5() { // call PackageChangedReceiver which then enables/disables OpenTasks sync when it's (not) available - SyncUtils.updateTaskSync(context) + TaskUtils.selectProvider(context, TaskUtils.currentProvider(context)) } @Suppress("unused") diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/Settings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/Settings.kt index db7ccdc0..769805fa 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/Settings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/Settings.kt @@ -8,8 +8,6 @@ import androidx.appcompat.app.AppCompatDelegate object Settings { - const val BATTERY_OPTIMIZATION = "battery_optimization" - const val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs" const val PROXY_TYPE = "proxy_type" // Integer @@ -43,7 +41,7 @@ object Settings { * 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. * - * If no tasks app is installed, this setting is not set. + * If no tasks app is available, this setting is not set. */ const val SELECTED_TASKS_PROVIDER = "preferred_tasks_provider" diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/JtxSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/JtxSyncer.kt index 95f3a3fd..a1d6bda9 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/JtxSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/JtxSyncer.kt @@ -15,6 +15,7 @@ import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.resource.LocalJtxCollection +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.ical4android.JtxCollection import at.bitfire.ical4android.TaskProvider @@ -62,7 +63,7 @@ class JtxSyncer(context: Context): Syncer(context) { } } catch (e: TaskProvider.ProviderTooOldException) { - SyncUtils.notifyProviderTooOld(context, e) + TaskUtils.notifyProviderTooOld(context, e) } catch (e: Exception) { Logger.log.log(Level.SEVERE, "Couldn't sync jtx collections", e) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncUtils.kt index 0e8e76bf..944b34b9 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncUtils.kt @@ -4,35 +4,14 @@ package at.bitfire.davdroid.syncadapter -import android.accounts.Account -import android.accounts.AccountManager -import android.app.PendingIntent -import android.content.ContentResolver import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.drawable.BitmapDrawable -import android.net.Uri import android.provider.CalendarContract -import androidx.annotation.WorkerThread -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase -import at.bitfire.davdroid.db.Service -import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.TaskUtils -import at.bitfire.davdroid.settings.AccountSettings -import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager -import at.bitfire.davdroid.ui.NotificationUtils -import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible -import at.bitfire.davdroid.util.PermissionUtils -import at.bitfire.ical4android.TaskProvider +import at.bitfire.davdroid.util.TaskUtils import dagger.hilt.EntryPoint import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent /** @@ -47,43 +26,6 @@ object SyncUtils { fun settingsManager(): SettingsManager } - /** - * Starts an Intent and redirects the user to the package in the market to update the app - * - * @param e the TaskProvider.ProviderTooOldException to be shown - */ - fun notifyProviderTooOld(context: Context, e: TaskProvider.ProviderTooOldException) { - val nm = NotificationManagerCompat.from(context) - val message = context.getString(R.string.sync_error_tasks_required_version, e.provider.minVersionName) - - val pm = context.packageManager - val tasksAppInfo = pm.getPackageInfo(e.provider.packageName, 0) - val tasksAppLabel = tasksAppInfo.applicationInfo.loadLabel(pm) - - val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS) - .setSmallIcon(R.drawable.ic_sync_problem_notify) - .setContentTitle(context.getString(R.string.sync_error_tasks_too_old, tasksAppLabel)) - .setContentText(message) - .setSubText("$tasksAppLabel ${e.installedVersionName}") - .setCategory(NotificationCompat.CATEGORY_ERROR) - - try { - val icon = pm.getApplicationIcon(e.provider.packageName) - if (icon is BitmapDrawable) - notify.setLargeIcon(icon.bitmap) - } catch (ignored: PackageManager.NameNotFoundException) { - // couldn't get provider app icon - } - - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${e.provider.packageName}")) - val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - - if (intent.resolveActivity(pm) != null) - notify.setContentIntent(PendingIntent.getActivity(context, 0, intent, flags)) - - nm.notifyIfPossible(NotificationUtils.NOTIFY_TASKS_PROVIDER_TOO_OLD, notify.build()) - } - /** * Returns a list of all available sync authorities: * @@ -107,61 +49,4 @@ object SyncUtils { return result } - // 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 - fun updateTaskSync(context: Context) { - val currentProvider = TaskUtils.currentProvider(context) - Logger.log.info("App launched or other package (un)installed; current tasks provider = $currentProvider") - - var permissionsRequired = false // whether additional permissions are required - - // 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 accountManager = AccountManager.get(context) - for (account in accountManager.getAccountsByType(context.getString(R.string.account_type))) { - val hasCalDAV = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) != null - for (providerName in TaskProvider.ProviderName.entries) { - val isSyncable = ContentResolver.getIsSyncable(account, providerName.authority) // may be -1 (unknown state) - val shallBeSyncable = hasCalDAV && providerName == currentProvider - if ((shallBeSyncable && isSyncable != 1) || (!shallBeSyncable && isSyncable != 0)) { - // enable/disable sync - setSyncableFromSettings(context, account, providerName.authority, shallBeSyncable) - - // if sync has just been enabled: check whether additional permissions are required - if (shallBeSyncable && !PermissionUtils.havePermissions(context, providerName.permissions)) - permissionsRequired = true - } - } - } - - if (permissionsRequired) { - Logger.log.warning("Tasks synchronization is now enabled for at least one account, but permissions are not granted") - PermissionUtils.notifyPermissions(context, null) - } - } - - private fun setSyncableFromSettings(context: Context, account: Account, authority: String, syncable: Boolean) { - val settingsManager by lazy { EntryPointAccessors.fromApplication(context, SyncUtilsEntryPoint::class.java).settingsManager() } - if (syncable) { - Logger.log.info("Enabling $authority sync for $account") - ContentResolver.setIsSyncable(account, authority, 1) - try { - val settings = AccountSettings(context, account) - val interval = settings.getTasksSyncInterval() ?: settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL) - settings.setSyncInterval(authority, interval) - } catch (e: InvalidAccountException) { - // account has already been removed - } - } else { - Logger.log.info("Disabling $authority sync for $account") - ContentResolver.setIsSyncable(account, authority, 0) - } - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/TaskSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/TaskSyncer.kt index cd6c5c2d..59d9b641 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/TaskSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/TaskSyncer.kt @@ -15,6 +15,7 @@ import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.resource.LocalTaskList +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.ical4android.DmfsTaskList import at.bitfire.ical4android.TaskProvider @@ -61,7 +62,7 @@ class TaskSyncer(context: Context): Syncer(context) { TasksSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, taskList).performSync() } } catch (e: TaskProvider.ProviderTooOldException) { - SyncUtils.notifyProviderTooOld(context, e) + TaskUtils.notifyProviderTooOld(context, e) syncResult.databaseError = true } catch (e: Exception) { Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt index 80f9dedb..bf6eae46 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AppSettingsActivity.kt @@ -63,7 +63,7 @@ import at.bitfire.cert4android.CustomCertStore import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.TaskUtils +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.ui.intro.BatteryOptimizationsPage diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt index bf8a0848..9c21fac8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksActivity.kt @@ -51,7 +51,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.PackageChangedReceiver import at.bitfire.davdroid.R -import at.bitfire.davdroid.resource.TaskUtils +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString import at.bitfire.davdroid.ui.widget.CardWithImage diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt index 5f653c55..69a93300 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountActivity.kt @@ -75,7 +75,7 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Collection -import at.bitfire.davdroid.resource.TaskUtils +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountModel.kt index 65901c3b..ef9ea5f1 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountModel.kt @@ -35,7 +35,7 @@ import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.resource.LocalTaskList -import at.bitfire.davdroid.resource.TaskUtils +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.syncadapter.AccountsCleanupWorker diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt index 17b334b5..4e24da92 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsActivity.kt @@ -63,7 +63,7 @@ import androidx.lifecycle.ViewModelProvider import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.TaskUtils +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt index b4e06448..671cd05e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/TasksIntroPage.kt @@ -7,7 +7,7 @@ package at.bitfire.davdroid.ui.intro import android.app.Application import androidx.compose.runtime.Composable import androidx.lifecycle.viewmodel.compose.viewModel -import at.bitfire.davdroid.resource.TaskUtils +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.ui.TasksActivity import at.bitfire.davdroid.ui.TasksCard diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index ea6a4171..9f87a070 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -32,7 +32,7 @@ import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.db.HomeSet import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.log.Logger -import at.bitfire.davdroid.resource.TaskUtils +import at.bitfire.davdroid.util.TaskUtils import at.bitfire.davdroid.servicedetection.DavResourceFinder import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/TaskUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/TaskUtils.kt new file mode 100644 index 00000000..6dcaabdd --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/TaskUtils.kt @@ -0,0 +1,200 @@ +/*************************************************************************************************** + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + **************************************************************************************************/ + +package at.bitfire.davdroid.util + +import android.accounts.Account +import android.accounts.AccountManager +import android.app.PendingIntent +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.drawable.BitmapDrawable +import android.net.Uri +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.map +import at.bitfire.davdroid.InvalidAccountException +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker +import at.bitfire.davdroid.syncadapter.SyncUtils +import at.bitfire.davdroid.ui.NotificationUtils +import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible +import at.bitfire.ical4android.TaskProvider +import at.bitfire.ical4android.TaskProvider.ProviderName +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent + +object TaskUtils { + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface TaskUtilsEntryPoint { + 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? { + val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager() + val preferredAuthority = settingsManager.getString(Settings.SELECTED_TASKS_PROVIDER) ?: return null + 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 { + val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager() + return settingsManager.getStringLive(Settings.SELECTED_TASKS_PROVIDER).map { preferred -> + if (preferred != null) + preferredAuthorityToProviderName(preferred, context.packageManager) + else + null + } + } + + private fun preferredAuthorityToProviderName( + preferredAuthority: String, + packageManager: PackageManager + ): ProviderName? { + ProviderName.entries.toTypedArray() + .sortedByDescending { it.authority == preferredAuthority } + .forEach { providerName -> + if (packageManager.resolveContentProvider(providerName.authority, 0) != null) + return providerName + } + return null + } + + fun isAvailable(context: Context) = currentProvider(context) != null + + /** + * Starts an Intent and redirects the user to the package in the market to update the app + * + * @param e the TaskProvider.ProviderTooOldException to be shown + */ + fun notifyProviderTooOld(context: Context, e: TaskProvider.ProviderTooOldException) { + val nm = NotificationManagerCompat.from(context) + val message = context.getString(R.string.sync_error_tasks_required_version, e.provider.minVersionName) + + val pm = context.packageManager + val tasksAppInfo = pm.getPackageInfo(e.provider.packageName, 0) + val tasksAppLabel = tasksAppInfo.applicationInfo.loadLabel(pm) + + val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_SYNC_ERRORS) + .setSmallIcon(R.drawable.ic_sync_problem_notify) + .setContentTitle(context.getString(R.string.sync_error_tasks_too_old, tasksAppLabel)) + .setContentText(message) + .setSubText("$tasksAppLabel ${e.installedVersionName}") + .setCategory(NotificationCompat.CATEGORY_ERROR) + + try { + val icon = pm.getApplicationIcon(e.provider.packageName) + if (icon is BitmapDrawable) + notify.setLargeIcon(icon.bitmap) + } catch (ignored: PackageManager.NameNotFoundException) { + // couldn't get provider app icon + } + + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${e.provider.packageName}")) + val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + + if (intent.resolveActivity(pm) != null) + notify.setContentIntent(PendingIntent.getActivity(context, 0, intent, flags)) + + nm.notifyIfPossible(NotificationUtils.NOTIFY_TASKS_PROVIDER_TOO_OLD, notify.build()) + } + + /** + * Sets up sync for the current TaskProvider (and disables sync for unavailable task providers): + * + * 1. Makes selected tasks authority _syncable_ in the sync framework, all other authorities _not syncable_. + * 2. Creates periodic sync worker for selected authority, disables periodic sync workers for all other authorities. + * 3. If the permissions don't allow synchronizing with the selected tasks app, a notification is shown. + * + * Called + * + * - when a user explicitly selects another task app, or + * - when there previously was no (usable) tasks app and [at.bitfire.davdroid.TasksWatcher] detected a new one. + */ + fun selectProvider(context: Context, selectedProvider: ProviderName?) { + Logger.log.info("Selecting tasks app: $selectedProvider") + + val settingsManager = EntryPointAccessors.fromApplication(context, TaskUtilsEntryPoint::class.java).settingsManager() + settingsManager.putString(Settings.SELECTED_TASKS_PROVIDER, selectedProvider?.authority) + + var permissionsRequired = false // whether additional permissions are required + + // check all accounts and (de)activate task provider(s) if a CalDAV service is defined + val db = EntryPointAccessors.fromApplication(context, SyncUtils.SyncUtilsEntryPoint::class.java).appDatabase() + val accountManager = AccountManager.get(context) + for (account in accountManager.getAccountsByType(context.getString(R.string.account_type))) { + val hasCalDAV = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) != null + for (providerName in TaskProvider.ProviderName.entries) { + val syncable = hasCalDAV && providerName == selectedProvider + + // enable/disable sync for the given account and authority + setSyncable( + context, + account, + providerName.authority, + syncable + ) + + // if sync has just been enabled: check whether additional permissions are required + if (syncable && !PermissionUtils.havePermissions(context, providerName.permissions)) + permissionsRequired = true + } + } + + if (permissionsRequired) { + Logger.log.warning("Tasks synchronization is now enabled for at least one account, but permissions are not granted") + PermissionUtils.notifyPermissions(context, null) + } + } + + private fun setSyncable(context: Context, account: Account, authority: String, syncable: Boolean) { + val settingsManager by lazy { EntryPointAccessors.fromApplication(context, SyncUtils.SyncUtilsEntryPoint::class.java).settingsManager() } + try { + val settings = AccountSettings(context, account) + if (syncable) { + Logger.log.info("Enabling $authority sync for $account") + + // make account syncable by sync framework + ContentResolver.setIsSyncable(account, authority, 1) + + // set sync interval according to settings; also updates periodic sync workers and sync framework on-content-change + val interval = settings.getTasksSyncInterval() ?: settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL) + settings.setSyncInterval(authority, interval) + } else { + Logger.log.info("Disabling $authority sync for $account") + + // make account not syncable by sync framework + ContentResolver.setIsSyncable(account, authority, 0) + + // disable periodic sync worker + PeriodicSyncWorker.disable(context, account, authority) + } + } catch (e: InvalidAccountException) { + // account has already been removed, make sure periodic sync is disabled, too + PeriodicSyncWorker.disable(context, account, authority) + } + } + +} \ No newline at end of file