Loading app/src/main/kotlin/at/bitfire/davdroid/App.kt +3 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener import at.bitfire.davdroid.syncadapter.SyncAdapterComponentManager import at.bitfire.davdroid.syncadapter.SyncUtils import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationUtils Loading Loading @@ -103,6 +104,8 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide // don't block UI for some background checks thread { SyncAdapterComponentManager.updateComponents(this) // watch for account changes/deletions accountsUpdatedListener.listen() Loading app/src/main/kotlin/at/bitfire/davdroid/TasksWatcher.kt +2 −0 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ package at.bitfire.davdroid import android.content.Context import android.content.Intent import at.bitfire.davdroid.syncadapter.SyncAdapterComponentManager import at.bitfire.davdroid.syncadapter.SyncUtils.updateTaskSync import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers Loading @@ -24,6 +25,7 @@ class TasksWatcher protected constructor( override fun onReceive(context: Context, intent: Intent) { CoroutineScope(Dispatchers.Default).launch { SyncAdapterComponentManager.updateComponents(context) updateTaskSync(context) } } Loading app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncAdapterComponentManager.kt 0 → 100644 +256 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package at.bitfire.davdroid.syncadapter import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import androidx.annotation.WorkerThread import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import java.util.logging.Level object SyncAdapterComponentManager { internal enum class ComponentStateUpdateDecision { SKIP_ALREADY_ALIGNED, SKIP_POLICY_CONTROLLED, UPDATE_REQUIRED } private data class SyncServiceSpec( val serviceClass: Class<*>, val authorityResId: Int ) private val optionalSyncServices = listOf( SyncServiceSpec(MurenaTasksSyncAdapterService::class.java, R.string.task_authority), SyncServiceSpec(MurenaNotesSyncAdapterService::class.java, R.string.notes_authority), SyncServiceSpec(MurenaEmailSyncAdapterService::class.java, R.string.email_authority), SyncServiceSpec(MurenaMediaSyncAdapterService::class.java, R.string.media_authority), SyncServiceSpec(MurenaAppDataSyncAdapterService::class.java, R.string.app_data_authority), SyncServiceSpec(MurenaMeteredEdriveSyncAdapterService::class.java, R.string.metered_edrive_authority), SyncServiceSpec(MurenaPasswordSyncAdapterService::class.java, R.string.password_authority), SyncServiceSpec(GoogleTasksSyncAdapterService::class.java, R.string.task_authority), SyncServiceSpec(GoogleEmailSyncAdapterService::class.java, R.string.email_authority), SyncServiceSpec(YahooTasksSyncAdapterService::class.java, R.string.task_authority), SyncServiceSpec(YahooEmailSyncAdapterService::class.java, R.string.email_authority) ) @WorkerThread fun updateComponents(context: Context) { val packageManager = context.packageManager optionalSyncServices .groupBy { context.getString(it.authorityResId) } .forEach { (authority, services) -> val isProviderAvailable = hasProvider(packageManager, authority) for (service in services) { updateSingleComponent( packageManager = packageManager, context = context, serviceClass = service.serviceClass, providerAvailable = isProviderAvailable, authority = authority ) } } } private fun hasProvider(packageManager: PackageManager, authority: String): Boolean { return packageManager.resolveContentProvider(authority, 0) != null } private fun updateSingleComponent( packageManager: PackageManager, context: Context, serviceClass: Class<*>, providerAvailable: Boolean, authority: String ) { val component = ComponentName(context, serviceClass) val manifestServiceEnabled = getManifestServiceEnabled(packageManager, component, serviceClass) if (manifestServiceEnabled == null) { return } val desiredState = resolveDesiredState( providerAvailable = providerAvailable, manifestServiceEnabled = manifestServiceEnabled ) val currentState = getComponentEnabledSetting(packageManager, component, serviceClass) if (currentState == null) { return } val updateDecision = decideComponentStateUpdate(currentState, desiredState) when (updateDecision) { ComponentStateUpdateDecision.SKIP_ALREADY_ALIGNED -> { return } ComponentStateUpdateDecision.SKIP_POLICY_CONTROLLED -> { Logger.log.log( Level.INFO, "Skipping ${serviceClass.simpleName}: preserving ${stateName(currentState)} (authority=$authority)" ) return } ComponentStateUpdateDecision.UPDATE_REQUIRED -> { val changed = setComponentEnabledSetting(packageManager, component, serviceClass, desiredState) if (!changed) { return } Logger.log.log( Level.INFO, "Set ${serviceClass.simpleName} to ${stateName(desiredState)} (authority=$authority)" ) } } } private fun getManifestServiceEnabled( packageManager: PackageManager, component: ComponentName, serviceClass: Class<*> ): Boolean? { return try { @Suppress("DEPRECATION") val serviceInfo = packageManager.getServiceInfo(component, 0) serviceInfo.enabled } catch (e: PackageManager.NameNotFoundException) { Logger.log.log( Level.WARNING, "Skipping ${serviceClass.simpleName}: service not declared in manifest", e ) null } catch (e: IllegalArgumentException) { Logger.log.log( Level.WARNING, "Skipping ${serviceClass.simpleName}: invalid component reference", e ) null } } private fun getComponentEnabledSetting( packageManager: PackageManager, component: ComponentName, serviceClass: Class<*> ): Int? { return try { packageManager.getComponentEnabledSetting(component) } catch (e: IllegalArgumentException) { Logger.log.log( Level.WARNING, "Skipping ${serviceClass.simpleName}: unable to read component state", e ) null } } private fun setComponentEnabledSetting( packageManager: PackageManager, component: ComponentName, serviceClass: Class<*>, state: Int ): Boolean { return try { packageManager.setComponentEnabledSetting( component, state, PackageManager.DONT_KILL_APP ) true } catch (e: IllegalArgumentException) { Logger.log.log( Level.WARNING, "Skipping ${serviceClass.simpleName}: unable to update component state", e ) false } } internal fun resolveDesiredState(providerAvailable: Boolean, manifestServiceEnabled: Boolean): Int { if (providerAvailable == manifestServiceEnabled) { return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT } if (providerAvailable) { return PackageManager.COMPONENT_ENABLED_STATE_ENABLED } return PackageManager.COMPONENT_ENABLED_STATE_DISABLED } internal fun decideComponentStateUpdate( currentState: Int, desiredState: Int ): ComponentStateUpdateDecision { if (currentState == desiredState) { return ComponentStateUpdateDecision.SKIP_ALREADY_ALIGNED } if (isPolicyControlledState(currentState)) { return ComponentStateUpdateDecision.SKIP_POLICY_CONTROLLED } return ComponentStateUpdateDecision.UPDATE_REQUIRED } internal fun isPolicyControlledState(componentState: Int): Boolean { return componentState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER || componentState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED } private fun stateName(state: Int): String { return when (state) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> { "ENABLED" } PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> { "DISABLED" } PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> { "DEFAULT" } PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> { "DISABLED_USER" } PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED -> { "DISABLED_UNTIL_USED" } else -> { "UNKNOWN($state)" } } } } app/src/test/kotlin/at/bitfire/davdroid/syncadapter/SyncAdapterComponentManagerTest.kt 0 → 100644 +120 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package at.bitfire.davdroid.syncadapter import android.content.pm.PackageManager import org.junit.Assert.assertEquals import org.junit.Test class SyncAdapterComponentManagerTest { @Test fun `resolveDesiredState returns default when provider and manifest match as enabled`() { val desiredState = SyncAdapterComponentManager.resolveDesiredState( providerAvailable = true, manifestServiceEnabled = true ) assertEquals(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, desiredState) } @Test fun `resolveDesiredState returns default when provider and manifest match as disabled`() { val desiredState = SyncAdapterComponentManager.resolveDesiredState( providerAvailable = false, manifestServiceEnabled = false ) assertEquals(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, desiredState) } @Test fun `resolveDesiredState returns enabled when provider is available and manifest is disabled`() { val desiredState = SyncAdapterComponentManager.resolveDesiredState( providerAvailable = true, manifestServiceEnabled = false ) assertEquals(PackageManager.COMPONENT_ENABLED_STATE_ENABLED, desiredState) } @Test fun `resolveDesiredState returns disabled when provider is unavailable and manifest is enabled`() { val desiredState = SyncAdapterComponentManager.resolveDesiredState( providerAvailable = false, manifestServiceEnabled = true ) assertEquals(PackageManager.COMPONENT_ENABLED_STATE_DISABLED, desiredState) } @Test fun `decideComponentStateUpdate skips when state is already aligned`() { val decision = SyncAdapterComponentManager.decideComponentStateUpdate( currentState = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, desiredState = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT ) assertEquals( SyncAdapterComponentManager.ComponentStateUpdateDecision.SKIP_ALREADY_ALIGNED, decision ) } @Test fun `decideComponentStateUpdate skips when state is disabled by user`() { val decision = SyncAdapterComponentManager.decideComponentStateUpdate( currentState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, desiredState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED ) assertEquals( SyncAdapterComponentManager.ComponentStateUpdateDecision.SKIP_POLICY_CONTROLLED, decision ) } @Test fun `decideComponentStateUpdate skips when state is disabled until used`() { val decision = SyncAdapterComponentManager.decideComponentStateUpdate( currentState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, desiredState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED ) assertEquals( SyncAdapterComponentManager.ComponentStateUpdateDecision.SKIP_POLICY_CONTROLLED, decision ) } @Test fun `decideComponentStateUpdate requests update when state is mutable and different`() { val decision = SyncAdapterComponentManager.decideComponentStateUpdate( currentState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED, desiredState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED ) assertEquals( SyncAdapterComponentManager.ComponentStateUpdateDecision.UPDATE_REQUIRED, decision ) } } Loading
app/src/main/kotlin/at/bitfire/davdroid/App.kt +3 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener import at.bitfire.davdroid.syncadapter.SyncAdapterComponentManager import at.bitfire.davdroid.syncadapter.SyncUtils import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationUtils Loading Loading @@ -103,6 +104,8 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide // don't block UI for some background checks thread { SyncAdapterComponentManager.updateComponents(this) // watch for account changes/deletions accountsUpdatedListener.listen() Loading
app/src/main/kotlin/at/bitfire/davdroid/TasksWatcher.kt +2 −0 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ package at.bitfire.davdroid import android.content.Context import android.content.Intent import at.bitfire.davdroid.syncadapter.SyncAdapterComponentManager import at.bitfire.davdroid.syncadapter.SyncUtils.updateTaskSync import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers Loading @@ -24,6 +25,7 @@ class TasksWatcher protected constructor( override fun onReceive(context: Context, intent: Intent) { CoroutineScope(Dispatchers.Default).launch { SyncAdapterComponentManager.updateComponents(context) updateTaskSync(context) } } Loading
app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncAdapterComponentManager.kt 0 → 100644 +256 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package at.bitfire.davdroid.syncadapter import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import androidx.annotation.WorkerThread import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import java.util.logging.Level object SyncAdapterComponentManager { internal enum class ComponentStateUpdateDecision { SKIP_ALREADY_ALIGNED, SKIP_POLICY_CONTROLLED, UPDATE_REQUIRED } private data class SyncServiceSpec( val serviceClass: Class<*>, val authorityResId: Int ) private val optionalSyncServices = listOf( SyncServiceSpec(MurenaTasksSyncAdapterService::class.java, R.string.task_authority), SyncServiceSpec(MurenaNotesSyncAdapterService::class.java, R.string.notes_authority), SyncServiceSpec(MurenaEmailSyncAdapterService::class.java, R.string.email_authority), SyncServiceSpec(MurenaMediaSyncAdapterService::class.java, R.string.media_authority), SyncServiceSpec(MurenaAppDataSyncAdapterService::class.java, R.string.app_data_authority), SyncServiceSpec(MurenaMeteredEdriveSyncAdapterService::class.java, R.string.metered_edrive_authority), SyncServiceSpec(MurenaPasswordSyncAdapterService::class.java, R.string.password_authority), SyncServiceSpec(GoogleTasksSyncAdapterService::class.java, R.string.task_authority), SyncServiceSpec(GoogleEmailSyncAdapterService::class.java, R.string.email_authority), SyncServiceSpec(YahooTasksSyncAdapterService::class.java, R.string.task_authority), SyncServiceSpec(YahooEmailSyncAdapterService::class.java, R.string.email_authority) ) @WorkerThread fun updateComponents(context: Context) { val packageManager = context.packageManager optionalSyncServices .groupBy { context.getString(it.authorityResId) } .forEach { (authority, services) -> val isProviderAvailable = hasProvider(packageManager, authority) for (service in services) { updateSingleComponent( packageManager = packageManager, context = context, serviceClass = service.serviceClass, providerAvailable = isProviderAvailable, authority = authority ) } } } private fun hasProvider(packageManager: PackageManager, authority: String): Boolean { return packageManager.resolveContentProvider(authority, 0) != null } private fun updateSingleComponent( packageManager: PackageManager, context: Context, serviceClass: Class<*>, providerAvailable: Boolean, authority: String ) { val component = ComponentName(context, serviceClass) val manifestServiceEnabled = getManifestServiceEnabled(packageManager, component, serviceClass) if (manifestServiceEnabled == null) { return } val desiredState = resolveDesiredState( providerAvailable = providerAvailable, manifestServiceEnabled = manifestServiceEnabled ) val currentState = getComponentEnabledSetting(packageManager, component, serviceClass) if (currentState == null) { return } val updateDecision = decideComponentStateUpdate(currentState, desiredState) when (updateDecision) { ComponentStateUpdateDecision.SKIP_ALREADY_ALIGNED -> { return } ComponentStateUpdateDecision.SKIP_POLICY_CONTROLLED -> { Logger.log.log( Level.INFO, "Skipping ${serviceClass.simpleName}: preserving ${stateName(currentState)} (authority=$authority)" ) return } ComponentStateUpdateDecision.UPDATE_REQUIRED -> { val changed = setComponentEnabledSetting(packageManager, component, serviceClass, desiredState) if (!changed) { return } Logger.log.log( Level.INFO, "Set ${serviceClass.simpleName} to ${stateName(desiredState)} (authority=$authority)" ) } } } private fun getManifestServiceEnabled( packageManager: PackageManager, component: ComponentName, serviceClass: Class<*> ): Boolean? { return try { @Suppress("DEPRECATION") val serviceInfo = packageManager.getServiceInfo(component, 0) serviceInfo.enabled } catch (e: PackageManager.NameNotFoundException) { Logger.log.log( Level.WARNING, "Skipping ${serviceClass.simpleName}: service not declared in manifest", e ) null } catch (e: IllegalArgumentException) { Logger.log.log( Level.WARNING, "Skipping ${serviceClass.simpleName}: invalid component reference", e ) null } } private fun getComponentEnabledSetting( packageManager: PackageManager, component: ComponentName, serviceClass: Class<*> ): Int? { return try { packageManager.getComponentEnabledSetting(component) } catch (e: IllegalArgumentException) { Logger.log.log( Level.WARNING, "Skipping ${serviceClass.simpleName}: unable to read component state", e ) null } } private fun setComponentEnabledSetting( packageManager: PackageManager, component: ComponentName, serviceClass: Class<*>, state: Int ): Boolean { return try { packageManager.setComponentEnabledSetting( component, state, PackageManager.DONT_KILL_APP ) true } catch (e: IllegalArgumentException) { Logger.log.log( Level.WARNING, "Skipping ${serviceClass.simpleName}: unable to update component state", e ) false } } internal fun resolveDesiredState(providerAvailable: Boolean, manifestServiceEnabled: Boolean): Int { if (providerAvailable == manifestServiceEnabled) { return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT } if (providerAvailable) { return PackageManager.COMPONENT_ENABLED_STATE_ENABLED } return PackageManager.COMPONENT_ENABLED_STATE_DISABLED } internal fun decideComponentStateUpdate( currentState: Int, desiredState: Int ): ComponentStateUpdateDecision { if (currentState == desiredState) { return ComponentStateUpdateDecision.SKIP_ALREADY_ALIGNED } if (isPolicyControlledState(currentState)) { return ComponentStateUpdateDecision.SKIP_POLICY_CONTROLLED } return ComponentStateUpdateDecision.UPDATE_REQUIRED } internal fun isPolicyControlledState(componentState: Int): Boolean { return componentState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER || componentState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED } private fun stateName(state: Int): String { return when (state) { PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> { "ENABLED" } PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> { "DISABLED" } PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> { "DEFAULT" } PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER -> { "DISABLED_USER" } PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED -> { "DISABLED_UNTIL_USED" } else -> { "UNKNOWN($state)" } } } }
app/src/test/kotlin/at/bitfire/davdroid/syncadapter/SyncAdapterComponentManagerTest.kt 0 → 100644 +120 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package at.bitfire.davdroid.syncadapter import android.content.pm.PackageManager import org.junit.Assert.assertEquals import org.junit.Test class SyncAdapterComponentManagerTest { @Test fun `resolveDesiredState returns default when provider and manifest match as enabled`() { val desiredState = SyncAdapterComponentManager.resolveDesiredState( providerAvailable = true, manifestServiceEnabled = true ) assertEquals(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, desiredState) } @Test fun `resolveDesiredState returns default when provider and manifest match as disabled`() { val desiredState = SyncAdapterComponentManager.resolveDesiredState( providerAvailable = false, manifestServiceEnabled = false ) assertEquals(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, desiredState) } @Test fun `resolveDesiredState returns enabled when provider is available and manifest is disabled`() { val desiredState = SyncAdapterComponentManager.resolveDesiredState( providerAvailable = true, manifestServiceEnabled = false ) assertEquals(PackageManager.COMPONENT_ENABLED_STATE_ENABLED, desiredState) } @Test fun `resolveDesiredState returns disabled when provider is unavailable and manifest is enabled`() { val desiredState = SyncAdapterComponentManager.resolveDesiredState( providerAvailable = false, manifestServiceEnabled = true ) assertEquals(PackageManager.COMPONENT_ENABLED_STATE_DISABLED, desiredState) } @Test fun `decideComponentStateUpdate skips when state is already aligned`() { val decision = SyncAdapterComponentManager.decideComponentStateUpdate( currentState = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, desiredState = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT ) assertEquals( SyncAdapterComponentManager.ComponentStateUpdateDecision.SKIP_ALREADY_ALIGNED, decision ) } @Test fun `decideComponentStateUpdate skips when state is disabled by user`() { val decision = SyncAdapterComponentManager.decideComponentStateUpdate( currentState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, desiredState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED ) assertEquals( SyncAdapterComponentManager.ComponentStateUpdateDecision.SKIP_POLICY_CONTROLLED, decision ) } @Test fun `decideComponentStateUpdate skips when state is disabled until used`() { val decision = SyncAdapterComponentManager.decideComponentStateUpdate( currentState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, desiredState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED ) assertEquals( SyncAdapterComponentManager.ComponentStateUpdateDecision.SKIP_POLICY_CONTROLLED, decision ) } @Test fun `decideComponentStateUpdate requests update when state is mutable and different`() { val decision = SyncAdapterComponentManager.decideComponentStateUpdate( currentState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED, desiredState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED ) assertEquals( SyncAdapterComponentManager.ComponentStateUpdateDecision.UPDATE_REQUIRED, decision ) } }