Loading packages/SettingsLib/Spa/spa/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ android_library { "androidx.compose.runtime_runtime-livedata", "androidx.compose.ui_ui-tooling-preview", "androidx.lifecycle_lifecycle-livedata-ktx", "androidx.lifecycle_lifecycle-runtime-compose", "androidx.navigation_navigation-compose", "com.google.android.material_material", "lottie_compose", Loading packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt +42 −20 Original line number Diff line number Diff line Loading @@ -20,27 +20,41 @@ import android.app.admin.DevicePolicyResources.Strings.Settings import android.content.Context import android.os.UserHandle import android.os.UserManager import androidx.lifecycle.liveData import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settingslib.RestrictedLockUtils import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.spaprivileged.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn data class Restrictions( val userId: Int, val keys: List<String>, ) sealed class RestrictedMode sealed interface RestrictedMode object NoRestricted : RestrictedMode() object NoRestricted : RestrictedMode object BaseUserRestricted : RestrictedMode() object BaseUserRestricted : RestrictedMode data class BlockedByAdmin( val enterpriseRepository: EnterpriseRepository, val enforcedAdmin: EnforcedAdmin, ) : RestrictedMode() { fun getSummary(checked: Boolean?): String = when (checked) { interface BlockedByAdmin : RestrictedMode { fun getSummary(checked: Boolean?): String fun sendShowAdminSupportDetailsIntent() } private data class BlockedByAdminImpl( private val context: Context, private val enforcedAdmin: EnforcedAdmin, ) : BlockedByAdmin { private val enterpriseRepository by lazy { EnterpriseRepository(context) } override fun getSummary(checked: Boolean?) = when (checked) { true -> enterpriseRepository.getEnterpriseString( Settings.ENABLED_BY_ADMIN_SWITCH_SUMMARY, R.string.enabled_by_admin ) Loading @@ -49,18 +63,31 @@ data class BlockedByAdmin( ) else -> "" } override fun sendShowAdminSupportDetailsIntent() { RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, enforcedAdmin) } } class RestrictionsProvider( interface RestrictionsProvider { @Composable fun restrictedModeState(): State<RestrictedMode?> } internal class RestrictionsProviderImpl( private val context: Context, private val restrictions: Restrictions, ) { ) : RestrictionsProvider { private val userManager by lazy { UserManager.get(context) } private val enterpriseRepository by lazy { EnterpriseRepository(context) } val restrictedMode = liveData { private val restrictedMode = flow { emit(getRestrictedMode()) } }.flowOn(Dispatchers.IO) @OptIn(ExperimentalLifecycleComposeApi::class) @Composable override fun restrictedModeState() = restrictedMode.collectAsStateWithLifecycle(initialValue = null) private fun getRestrictedMode(): RestrictedMode { for (key in restrictions.keys) { Loading @@ -71,12 +98,7 @@ class RestrictionsProvider( for (key in restrictions.keys) { RestrictedLockUtilsInternal .checkIfRestrictionEnforced(context, key, restrictions.userId) ?.let { return BlockedByAdmin( enterpriseRepository = enterpriseRepository, enforcedAdmin = it, ) } ?.let { return BlockedByAdminImpl(context = context, enforcedAdmin = it) } } return NoRestricted } Loading packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt +7 −10 Original line number Diff line number Diff line Loading @@ -22,7 +22,6 @@ import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource Loading @@ -43,7 +42,7 @@ import com.android.settingslib.spaprivileged.model.app.AppListModel import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference import kotlinx.coroutines.flow.Flow Loading Loading @@ -146,9 +145,7 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>( listModel.filter(userIdFlow, recordListFlow) @Composable override fun getSummary(option: Int, record: T): State<String> { return getSummary(record) } override fun getSummary(option: Int, record: T) = getSummary(record) @Composable fun getSummary(record: T): State<String> { Loading @@ -157,27 +154,27 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>( userId = record.app.userId, keys = listModel.switchRestrictionKeys, ) RestrictionsProvider(context, restrictions) RestrictionsProviderImpl(context, restrictions) } val restrictedMode = restrictionsProvider.restrictedMode.observeAsState() val restrictedMode = restrictionsProvider.restrictedModeState() val allowed = listModel.isAllowed(record) return remember { derivedStateOf { RestrictedSwitchPreference.getSummary( context = context, restrictedMode = restrictedMode.value, noRestrictedSummary = getNoRestrictedSummary(allowed), summaryIfNoRestricted = getSummaryIfNoRestricted(allowed), checked = allowed, ).value } } } private fun getNoRestrictedSummary(allowed: State<Boolean?>) = derivedStateOf { private fun getSummaryIfNoRestricted(allowed: State<Boolean?>) = derivedStateOf { when (allowed.value) { true -> context.getString(R.string.app_permission_summary_allowed) false -> context.getString(R.string.app_permission_summary_not_allowed) else -> context.getString(R.string.summary_placeholder) null -> context.getString(R.string.summary_placeholder) } } } packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt +52 −18 Original line number Diff line number Diff line Loading @@ -22,12 +22,13 @@ import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.Role import com.android.settingslib.RestrictedLockUtils import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.state.ToggleableState import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.widget.preference.SwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel Loading @@ -38,32 +39,44 @@ import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl @Composable fun RestrictedSwitchPreference(model: SwitchPreferenceModel, restrictions: Restrictions) { RestrictedSwitchPreferenceImpl(model, restrictions, ::RestrictionsProviderImpl) } @Composable internal fun RestrictedSwitchPreferenceImpl( model: SwitchPreferenceModel, restrictions: Restrictions, restrictionsProviderFactory: (Context, Restrictions) -> RestrictionsProvider, ) { if (restrictions.keys.isEmpty()) { SwitchPreference(model) return } val context = LocalContext.current val restrictionsProvider = remember { RestrictionsProvider(context, restrictions) } val restrictedMode = restrictionsProvider.restrictedMode.observeAsState().value ?: return val restrictionsProvider = remember(restrictions) { restrictionsProviderFactory(context, restrictions) } val restrictedMode = restrictionsProvider.restrictedModeState().value val restrictedSwitchModel = remember(restrictedMode) { RestrictedSwitchPreferenceModel(context, model, restrictedMode) } Box(remember { restrictedSwitchModel.getModifier() }) { restrictedSwitchModel.RestrictionWrapper { SwitchPreference(restrictedSwitchModel) } } object RestrictedSwitchPreference { internal object RestrictedSwitchPreference { fun getSummary( context: Context, restrictedMode: RestrictedMode?, noRestrictedSummary: State<String>, summaryIfNoRestricted: State<String>, checked: State<Boolean?>, ): State<String> = when (restrictedMode) { is NoRestricted -> noRestrictedSummary is NoRestricted -> summaryIfNoRestricted is BaseUserRestricted -> stateOf(context.getString(R.string.disabled)) is BlockedByAdmin -> derivedStateOf { restrictedMode.getSummary(checked.value) } null -> stateOf(context.getString(R.string.summary_placeholder)) Loading @@ -71,43 +84,64 @@ object RestrictedSwitchPreference { } private class RestrictedSwitchPreferenceModel( private val context: Context, context: Context, model: SwitchPreferenceModel, private val restrictedMode: RestrictedMode, private val restrictedMode: RestrictedMode?, ) : SwitchPreferenceModel { override val title = model.title override val summary = RestrictedSwitchPreference.getSummary( context = context, restrictedMode = restrictedMode, noRestrictedSummary = model.summary, summaryIfNoRestricted = model.summary, checked = model.checked, ) override val checked = when (restrictedMode) { null -> stateOf(null) is NoRestricted -> model.checked is BaseUserRestricted -> stateOf(false) is BlockedByAdmin -> model.checked } override val changeable = when (restrictedMode) { null -> stateOf(false) is NoRestricted -> model.changeable is BaseUserRestricted -> stateOf(false) is BlockedByAdmin -> stateOf(false) } override val onCheckedChange = when (restrictedMode) { null -> null is NoRestricted -> model.onCheckedChange is BaseUserRestricted -> null // Need to pass a non null onCheckedChange to enable semantics ToggleableState, although // since changeable is false this will not be called. is BaseUserRestricted -> model.onCheckedChange // Pass null since semantics ToggleableState is provided in RestrictionWrapper. is BlockedByAdmin -> null } fun getModifier(): Modifier = when (restrictedMode) { is BlockedByAdmin -> Modifier.clickable(role = Role.Switch) { RestrictedLockUtils.sendShowAdminSupportDetailsIntent( context, restrictedMode.enforcedAdmin @Composable fun RestrictionWrapper(content: @Composable () -> Unit) { if (restrictedMode !is BlockedByAdmin) { content() return } Box( Modifier .clickable( role = Role.Switch, onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() }, ) .semantics { this.toggleableState = ToggleableState(checked.value) }, ) { content() } } else -> Modifier private fun ToggleableState(value: Boolean?) = when (value) { true -> ToggleableState.On false -> ToggleableState.Off null -> ToggleableState.Indeterminate } } packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt 0 → 100644 +175 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settingslib.spaprivileged.template.preference import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.isOff import androidx.compose.ui.test.isOn import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RestrictedSwitchPreferenceTest { @get:Rule val composeTestRule = createComposeRule() private val fakeBlockedByAdmin = object : BlockedByAdmin { var sendShowAdminSupportDetailsIntentIsCalled = false override fun getSummary(checked: Boolean?) = BLOCKED_BY_ADMIN_SUMMARY override fun sendShowAdminSupportDetailsIntent() { sendShowAdminSupportDetailsIntentIsCalled = true } } private val fakeRestrictionsProvider = FakeRestrictionsProvider() private val switchPreferenceModel = object : SwitchPreferenceModel { override val title = TITLE override val checked = mutableStateOf(true) override val onCheckedChange: (Boolean) -> Unit = { checked.value = it } } @Test fun whenRestrictionsKeysIsEmpty_enabled() { val restrictions = Restrictions(userId = USER_ID, keys = emptyList()) setContent(restrictions) composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled() composeTestRule.onNode(isOn()).assertIsDisplayed() } @Test fun whenRestrictionsKeysIsEmpty_toggleable() { val restrictions = Restrictions(userId = USER_ID, keys = emptyList()) setContent(restrictions) composeTestRule.onRoot().performClick() composeTestRule.onNode(isOff()).assertIsDisplayed() } @Test fun whenNoRestricted_enabled() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = NoRestricted setContent(restrictions) composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled() composeTestRule.onNode(isOn()).assertIsDisplayed() } @Test fun whenNoRestricted_toggleable() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = NoRestricted setContent(restrictions) composeTestRule.onRoot().performClick() composeTestRule.onNode(isOff()).assertIsDisplayed() } @Test fun whenBaseUserRestricted_disabled() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = BaseUserRestricted setContent(restrictions) composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsNotEnabled() composeTestRule.onNode(isOff()).assertIsDisplayed() } @Test fun whenBaseUserRestricted_notToggleable() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = BaseUserRestricted setContent(restrictions) composeTestRule.onRoot().performClick() composeTestRule.onNode(isOff()).assertIsDisplayed() } @Test fun whenBlockedByAdmin_disabled() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = fakeBlockedByAdmin setContent(restrictions) composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled() composeTestRule.onNodeWithText(BLOCKED_BY_ADMIN_SUMMARY).assertIsDisplayed() composeTestRule.onNode(isOn()).assertIsDisplayed() } @Test fun whenBlockedByAdmin_click() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = fakeBlockedByAdmin setContent(restrictions) composeTestRule.onRoot().performClick() assertThat(fakeBlockedByAdmin.sendShowAdminSupportDetailsIntentIsCalled).isTrue() } private fun setContent(restrictions: Restrictions) { composeTestRule.setContent { RestrictedSwitchPreferenceImpl(switchPreferenceModel, restrictions) { _, _ -> fakeRestrictionsProvider } } } private companion object { const val TITLE = "Title" const val USER_ID = 0 const val RESTRICTION_KEY = "restriction_key" const val BLOCKED_BY_ADMIN_SUMMARY = "Blocked by admin" } } private class FakeRestrictionsProvider : RestrictionsProvider { var restrictedMode: RestrictedMode? = null @Composable override fun restrictedModeState() = stateOf(restrictedMode) } Loading
packages/SettingsLib/Spa/spa/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ android_library { "androidx.compose.runtime_runtime-livedata", "androidx.compose.ui_ui-tooling-preview", "androidx.lifecycle_lifecycle-livedata-ktx", "androidx.lifecycle_lifecycle-runtime-compose", "androidx.navigation_navigation-compose", "com.google.android.material_material", "lottie_compose", Loading
packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt +42 −20 Original line number Diff line number Diff line Loading @@ -20,27 +20,41 @@ import android.app.admin.DevicePolicyResources.Strings.Settings import android.content.Context import android.os.UserHandle import android.os.UserManager import androidx.lifecycle.liveData import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settingslib.RestrictedLockUtils import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin import com.android.settingslib.RestrictedLockUtilsInternal import com.android.settingslib.spaprivileged.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn data class Restrictions( val userId: Int, val keys: List<String>, ) sealed class RestrictedMode sealed interface RestrictedMode object NoRestricted : RestrictedMode() object NoRestricted : RestrictedMode object BaseUserRestricted : RestrictedMode() object BaseUserRestricted : RestrictedMode data class BlockedByAdmin( val enterpriseRepository: EnterpriseRepository, val enforcedAdmin: EnforcedAdmin, ) : RestrictedMode() { fun getSummary(checked: Boolean?): String = when (checked) { interface BlockedByAdmin : RestrictedMode { fun getSummary(checked: Boolean?): String fun sendShowAdminSupportDetailsIntent() } private data class BlockedByAdminImpl( private val context: Context, private val enforcedAdmin: EnforcedAdmin, ) : BlockedByAdmin { private val enterpriseRepository by lazy { EnterpriseRepository(context) } override fun getSummary(checked: Boolean?) = when (checked) { true -> enterpriseRepository.getEnterpriseString( Settings.ENABLED_BY_ADMIN_SWITCH_SUMMARY, R.string.enabled_by_admin ) Loading @@ -49,18 +63,31 @@ data class BlockedByAdmin( ) else -> "" } override fun sendShowAdminSupportDetailsIntent() { RestrictedLockUtils.sendShowAdminSupportDetailsIntent(context, enforcedAdmin) } } class RestrictionsProvider( interface RestrictionsProvider { @Composable fun restrictedModeState(): State<RestrictedMode?> } internal class RestrictionsProviderImpl( private val context: Context, private val restrictions: Restrictions, ) { ) : RestrictionsProvider { private val userManager by lazy { UserManager.get(context) } private val enterpriseRepository by lazy { EnterpriseRepository(context) } val restrictedMode = liveData { private val restrictedMode = flow { emit(getRestrictedMode()) } }.flowOn(Dispatchers.IO) @OptIn(ExperimentalLifecycleComposeApi::class) @Composable override fun restrictedModeState() = restrictedMode.collectAsStateWithLifecycle(initialValue = null) private fun getRestrictedMode(): RestrictedMode { for (key in restrictions.keys) { Loading @@ -71,12 +98,7 @@ class RestrictionsProvider( for (key in restrictions.keys) { RestrictedLockUtilsInternal .checkIfRestrictionEnforced(context, key, restrictions.userId) ?.let { return BlockedByAdmin( enterpriseRepository = enterpriseRepository, enforcedAdmin = it, ) } ?.let { return BlockedByAdminImpl(context = context, enforcedAdmin = it) } } return NoRestricted } Loading
packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt +7 −10 Original line number Diff line number Diff line Loading @@ -22,7 +22,6 @@ import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource Loading @@ -43,7 +42,7 @@ import com.android.settingslib.spaprivileged.model.app.AppListModel import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference import kotlinx.coroutines.flow.Flow Loading Loading @@ -146,9 +145,7 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>( listModel.filter(userIdFlow, recordListFlow) @Composable override fun getSummary(option: Int, record: T): State<String> { return getSummary(record) } override fun getSummary(option: Int, record: T) = getSummary(record) @Composable fun getSummary(record: T): State<String> { Loading @@ -157,27 +154,27 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>( userId = record.app.userId, keys = listModel.switchRestrictionKeys, ) RestrictionsProvider(context, restrictions) RestrictionsProviderImpl(context, restrictions) } val restrictedMode = restrictionsProvider.restrictedMode.observeAsState() val restrictedMode = restrictionsProvider.restrictedModeState() val allowed = listModel.isAllowed(record) return remember { derivedStateOf { RestrictedSwitchPreference.getSummary( context = context, restrictedMode = restrictedMode.value, noRestrictedSummary = getNoRestrictedSummary(allowed), summaryIfNoRestricted = getSummaryIfNoRestricted(allowed), checked = allowed, ).value } } } private fun getNoRestrictedSummary(allowed: State<Boolean?>) = derivedStateOf { private fun getSummaryIfNoRestricted(allowed: State<Boolean?>) = derivedStateOf { when (allowed.value) { true -> context.getString(R.string.app_permission_summary_allowed) false -> context.getString(R.string.app_permission_summary_not_allowed) else -> context.getString(R.string.summary_placeholder) null -> context.getString(R.string.summary_placeholder) } } }
packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt +52 −18 Original line number Diff line number Diff line Loading @@ -22,12 +22,13 @@ import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.Role import com.android.settingslib.RestrictedLockUtils import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.state.ToggleableState import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.widget.preference.SwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel Loading @@ -38,32 +39,44 @@ import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl @Composable fun RestrictedSwitchPreference(model: SwitchPreferenceModel, restrictions: Restrictions) { RestrictedSwitchPreferenceImpl(model, restrictions, ::RestrictionsProviderImpl) } @Composable internal fun RestrictedSwitchPreferenceImpl( model: SwitchPreferenceModel, restrictions: Restrictions, restrictionsProviderFactory: (Context, Restrictions) -> RestrictionsProvider, ) { if (restrictions.keys.isEmpty()) { SwitchPreference(model) return } val context = LocalContext.current val restrictionsProvider = remember { RestrictionsProvider(context, restrictions) } val restrictedMode = restrictionsProvider.restrictedMode.observeAsState().value ?: return val restrictionsProvider = remember(restrictions) { restrictionsProviderFactory(context, restrictions) } val restrictedMode = restrictionsProvider.restrictedModeState().value val restrictedSwitchModel = remember(restrictedMode) { RestrictedSwitchPreferenceModel(context, model, restrictedMode) } Box(remember { restrictedSwitchModel.getModifier() }) { restrictedSwitchModel.RestrictionWrapper { SwitchPreference(restrictedSwitchModel) } } object RestrictedSwitchPreference { internal object RestrictedSwitchPreference { fun getSummary( context: Context, restrictedMode: RestrictedMode?, noRestrictedSummary: State<String>, summaryIfNoRestricted: State<String>, checked: State<Boolean?>, ): State<String> = when (restrictedMode) { is NoRestricted -> noRestrictedSummary is NoRestricted -> summaryIfNoRestricted is BaseUserRestricted -> stateOf(context.getString(R.string.disabled)) is BlockedByAdmin -> derivedStateOf { restrictedMode.getSummary(checked.value) } null -> stateOf(context.getString(R.string.summary_placeholder)) Loading @@ -71,43 +84,64 @@ object RestrictedSwitchPreference { } private class RestrictedSwitchPreferenceModel( private val context: Context, context: Context, model: SwitchPreferenceModel, private val restrictedMode: RestrictedMode, private val restrictedMode: RestrictedMode?, ) : SwitchPreferenceModel { override val title = model.title override val summary = RestrictedSwitchPreference.getSummary( context = context, restrictedMode = restrictedMode, noRestrictedSummary = model.summary, summaryIfNoRestricted = model.summary, checked = model.checked, ) override val checked = when (restrictedMode) { null -> stateOf(null) is NoRestricted -> model.checked is BaseUserRestricted -> stateOf(false) is BlockedByAdmin -> model.checked } override val changeable = when (restrictedMode) { null -> stateOf(false) is NoRestricted -> model.changeable is BaseUserRestricted -> stateOf(false) is BlockedByAdmin -> stateOf(false) } override val onCheckedChange = when (restrictedMode) { null -> null is NoRestricted -> model.onCheckedChange is BaseUserRestricted -> null // Need to pass a non null onCheckedChange to enable semantics ToggleableState, although // since changeable is false this will not be called. is BaseUserRestricted -> model.onCheckedChange // Pass null since semantics ToggleableState is provided in RestrictionWrapper. is BlockedByAdmin -> null } fun getModifier(): Modifier = when (restrictedMode) { is BlockedByAdmin -> Modifier.clickable(role = Role.Switch) { RestrictedLockUtils.sendShowAdminSupportDetailsIntent( context, restrictedMode.enforcedAdmin @Composable fun RestrictionWrapper(content: @Composable () -> Unit) { if (restrictedMode !is BlockedByAdmin) { content() return } Box( Modifier .clickable( role = Role.Switch, onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() }, ) .semantics { this.toggleableState = ToggleableState(checked.value) }, ) { content() } } else -> Modifier private fun ToggleableState(value: Boolean?) = when (value) { true -> ToggleableState.On false -> ToggleableState.Off null -> ToggleableState.Indeterminate } }
packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt 0 → 100644 +175 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settingslib.spaprivileged.template.preference import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.isOff import androidx.compose.ui.test.isOn import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RestrictedSwitchPreferenceTest { @get:Rule val composeTestRule = createComposeRule() private val fakeBlockedByAdmin = object : BlockedByAdmin { var sendShowAdminSupportDetailsIntentIsCalled = false override fun getSummary(checked: Boolean?) = BLOCKED_BY_ADMIN_SUMMARY override fun sendShowAdminSupportDetailsIntent() { sendShowAdminSupportDetailsIntentIsCalled = true } } private val fakeRestrictionsProvider = FakeRestrictionsProvider() private val switchPreferenceModel = object : SwitchPreferenceModel { override val title = TITLE override val checked = mutableStateOf(true) override val onCheckedChange: (Boolean) -> Unit = { checked.value = it } } @Test fun whenRestrictionsKeysIsEmpty_enabled() { val restrictions = Restrictions(userId = USER_ID, keys = emptyList()) setContent(restrictions) composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled() composeTestRule.onNode(isOn()).assertIsDisplayed() } @Test fun whenRestrictionsKeysIsEmpty_toggleable() { val restrictions = Restrictions(userId = USER_ID, keys = emptyList()) setContent(restrictions) composeTestRule.onRoot().performClick() composeTestRule.onNode(isOff()).assertIsDisplayed() } @Test fun whenNoRestricted_enabled() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = NoRestricted setContent(restrictions) composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled() composeTestRule.onNode(isOn()).assertIsDisplayed() } @Test fun whenNoRestricted_toggleable() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = NoRestricted setContent(restrictions) composeTestRule.onRoot().performClick() composeTestRule.onNode(isOff()).assertIsDisplayed() } @Test fun whenBaseUserRestricted_disabled() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = BaseUserRestricted setContent(restrictions) composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsNotEnabled() composeTestRule.onNode(isOff()).assertIsDisplayed() } @Test fun whenBaseUserRestricted_notToggleable() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = BaseUserRestricted setContent(restrictions) composeTestRule.onRoot().performClick() composeTestRule.onNode(isOff()).assertIsDisplayed() } @Test fun whenBlockedByAdmin_disabled() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = fakeBlockedByAdmin setContent(restrictions) composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled() composeTestRule.onNodeWithText(BLOCKED_BY_ADMIN_SUMMARY).assertIsDisplayed() composeTestRule.onNode(isOn()).assertIsDisplayed() } @Test fun whenBlockedByAdmin_click() { val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) fakeRestrictionsProvider.restrictedMode = fakeBlockedByAdmin setContent(restrictions) composeTestRule.onRoot().performClick() assertThat(fakeBlockedByAdmin.sendShowAdminSupportDetailsIntentIsCalled).isTrue() } private fun setContent(restrictions: Restrictions) { composeTestRule.setContent { RestrictedSwitchPreferenceImpl(switchPreferenceModel, restrictions) { _, _ -> fakeRestrictionsProvider } } } private companion object { const val TITLE = "Title" const val USER_ID = 0 const val RESTRICTION_KEY = "restriction_key" const val BLOCKED_BY_ADMIN_SUMMARY = "Blocked by admin" } } private class FakeRestrictionsProvider : RestrictionsProvider { var restrictedMode: RestrictedMode? = null @Composable override fun restrictedModeState() = stateOf(restrictedMode) }