diff options
6 files changed, 290 insertions, 22 deletions
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt index f54de1514fcf..09cb98e22af3 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictionsProvider.kt @@ -22,6 +22,8 @@ import android.os.UserHandle import android.os.UserManager import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settingslib.RestrictedLockUtils import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin @@ -32,15 +34,15 @@ import kotlinx.coroutines.flow.flowOn import com.android.settingslib.widget.restricted.R data class Restrictions( - val userId: Int, + val userId: Int = UserHandle.myUserId(), val keys: List<String>, ) sealed interface RestrictedMode -object NoRestricted : RestrictedMode +data object NoRestricted : RestrictedMode -object BaseUserRestricted : RestrictedMode +data object BaseUserRestricted : RestrictedMode interface BlockedByAdmin : RestrictedMode { fun getSummary(checked: Boolean?): String @@ -79,6 +81,17 @@ interface RestrictionsProvider { typealias RestrictionsProviderFactory = (Context, Restrictions) -> RestrictionsProvider +@Composable +internal fun RestrictionsProviderFactory.rememberRestrictedMode( + restrictions: Restrictions, +): State<RestrictedMode?> { + val context = LocalContext.current + val restrictionsProvider = remember(restrictions) { + this(context, restrictions) + } + return restrictionsProvider.restrictedModeState() +} + internal class RestrictionsProviderImpl( private val context: Context, private val restrictions: Restrictions, diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt index 1fa854a4c09e..17e970845f58 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt @@ -45,6 +45,7 @@ import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl +import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference import kotlinx.coroutines.flow.Flow @@ -149,14 +150,13 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>( @Composable fun getSummary(record: T): State<String> { - val restrictionsProvider = remember(record.app.userId) { - val restrictions = Restrictions( + val restrictions = remember(record.app.userId) { + Restrictions( userId = record.app.userId, keys = listModel.switchRestrictionKeys, ) - restrictionsProviderFactory(context, restrictions) } - val restrictedMode = restrictionsProvider.restrictedModeState() + val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions) val allowed = listModel.isAllowed(record) return remember { derivedStateOf { diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreference.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreference.kt new file mode 100644 index 000000000000..50490c0b887d --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreference.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 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.annotation.VisibleForTesting +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +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.RestrictionsProviderFactory +import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl +import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode + +@Composable +fun RestrictedPreference( + model: PreferenceModel, + restrictions: Restrictions, +) { + RestrictedPreference(model, restrictions, ::RestrictionsProviderImpl) +} + +@VisibleForTesting +@Composable +internal fun RestrictedPreference( + model: PreferenceModel, + restrictions: Restrictions, + restrictionsProviderFactory: RestrictionsProviderFactory, +) { + if (restrictions.keys.isEmpty()) { + Preference(model) + return + } + val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value + val restrictedSwitchModel = remember(restrictedMode) { + RestrictedPreferenceModel(model, restrictedMode) + } + restrictedSwitchModel.RestrictionWrapper { + Preference(restrictedSwitchModel) + } +} + +private class RestrictedPreferenceModel( + model: PreferenceModel, + private val restrictedMode: RestrictedMode?, +) : PreferenceModel { + override val title = model.title + override val summary = model.summary + override val icon = model.icon + + override val enabled = when (restrictedMode) { + NoRestricted -> model.enabled + else -> stateOf(false) + } + + override val onClick = when (restrictedMode) { + NoRestricted -> model.onClick + // Need to passthrough onClick for clickable semantics, although since enabled is false so + // this will not be called. + BaseUserRestricted -> model.onClick + else -> null + } + + @Composable + fun RestrictionWrapper(content: @Composable () -> Unit) { + if (restrictedMode !is BlockedByAdmin) { + content() + return + } + Box( + Modifier + .clickable( + role = Role.Button, + onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() }, + ) + ) { content() } + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt index e77dcd4d9cc4..2129403c2d85 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt @@ -17,6 +17,7 @@ package com.android.settingslib.spaprivileged.template.preference import android.content.Context +import androidx.annotation.VisibleForTesting import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable @@ -40,22 +41,29 @@ import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl +import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode @Composable fun RestrictedSwitchPreference( model: SwitchPreferenceModel, restrictions: Restrictions, - restrictionsProviderFactory: RestrictionsProviderFactory = ::RestrictionsProviderImpl, +) { + RestrictedSwitchPreference(model, restrictions, ::RestrictionsProviderImpl) +} + +@VisibleForTesting +@Composable +internal fun RestrictedSwitchPreference( + model: SwitchPreferenceModel, + restrictions: Restrictions, + restrictionsProviderFactory: RestrictionsProviderFactory, ) { if (restrictions.keys.isEmpty()) { SwitchPreference(model) return } val context = LocalContext.current - val restrictionsProvider = remember(restrictions) { - restrictionsProviderFactory(context, restrictions) - } - val restrictedMode = restrictionsProvider.restrictedModeState().value + val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value val restrictedSwitchModel = remember(restrictedMode) { RestrictedSwitchPreferenceModel(context, model, restrictedMode) } @@ -112,8 +120,8 @@ private class RestrictedSwitchPreferenceModel( override val onCheckedChange = when (restrictedMode) { null -> null is NoRestricted -> model.onCheckedChange - // Need to pass a non null onCheckedChange to enable semantics ToggleableState, although - // since changeable is false this will not be called. + // Need to passthrough onCheckedChange for toggleable semantics, although since changeable + // is false so this will not be called. is BaseUserRestricted -> model.onCheckedChange // Pass null since semantics ToggleableState is provided in RestrictionWrapper. is BlockedByAdmin -> null diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt index 86b6f027997d..f9abefc11e24 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt @@ -16,15 +16,15 @@ package com.android.settingslib.spaprivileged.template.scaffold +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import com.android.settingslib.spa.widget.scaffold.MoreOptionsScope import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl +import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode @Composable fun MoreOptionsScope.RestrictedMenuItem( @@ -35,6 +35,7 @@ fun MoreOptionsScope.RestrictedMenuItem( RestrictedMenuItemImpl(text, restrictions, onClick, ::RestrictionsProviderImpl) } +@VisibleForTesting @Composable internal fun MoreOptionsScope.RestrictedMenuItemImpl( text: String, @@ -42,12 +43,8 @@ internal fun MoreOptionsScope.RestrictedMenuItemImpl( onClick: () -> Unit, restrictionsProviderFactory: RestrictionsProviderFactory, ) { - val context = LocalContext.current - val restrictionsProvider = remember(restrictions) { - restrictionsProviderFactory(context, restrictions) - } - val restrictedMode = restrictionsProvider.restrictedModeState().value - MenuItem(text = text, enabled = restrictedMode !is BaseUserRestricted) { + val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value + MenuItem(text = text, enabled = restrictedMode !== BaseUserRestricted) { when (restrictedMode) { is BlockedByAdmin -> restrictedMode.sendShowAdminSupportDetailsIntent() else -> onClick() diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreferenceTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreferenceTest.kt new file mode 100644 index 000000000000..eadf0ca0686d --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreferenceTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2023 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.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +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.widget.preference.PreferenceModel +import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted +import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted +import com.android.settingslib.spaprivileged.model.enterprise.Restrictions +import com.android.settingslib.spaprivileged.tests.testutils.FakeBlockedByAdmin +import com.android.settingslib.spaprivileged.tests.testutils.FakeRestrictionsProvider +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RestrictedPreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + private val fakeBlockedByAdmin = FakeBlockedByAdmin() + + private val fakeRestrictionsProvider = FakeRestrictionsProvider() + + private var clicked = false + + private val preferenceModel = object : PreferenceModel { + override val title = TITLE + override val onClick = { clicked = true } + } + + @Test + fun whenRestrictionsKeysIsEmpty_enabled() { + val restrictions = Restrictions(userId = USER_ID, keys = emptyList()) + + setContent(restrictions) + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled() + } + + @Test + fun whenRestrictionsKeysIsEmpty_clickable() { + val restrictions = Restrictions(userId = USER_ID, keys = emptyList()) + + setContent(restrictions) + composeTestRule.onRoot().performClick() + + assertThat(clicked).isTrue() + } + + @Test + fun whenNoRestricted_enabled() { + val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) + fakeRestrictionsProvider.restrictedMode = NoRestricted + + setContent(restrictions) + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled() + } + + @Test + fun whenNoRestricted_clickable() { + val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) + fakeRestrictionsProvider.restrictedMode = NoRestricted + + setContent(restrictions) + composeTestRule.onRoot().performClick() + + assertThat(clicked).isTrue() + } + + @Test + fun whenBaseUserRestricted_disabled() { + val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) + fakeRestrictionsProvider.restrictedMode = BaseUserRestricted + + setContent(restrictions) + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsNotEnabled() + } + + @Test + fun whenBaseUserRestricted_notClickable() { + val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) + fakeRestrictionsProvider.restrictedMode = BaseUserRestricted + + setContent(restrictions) + composeTestRule.onRoot().performClick() + + assertThat(clicked).isFalse() + } + + @Test + fun whenBlockedByAdmin_widgetInEnableStateToAllowClick() { + val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY)) + fakeRestrictionsProvider.restrictedMode = fakeBlockedByAdmin + + setContent(restrictions) + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled() + } + + @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 { + RestrictedPreference(preferenceModel, restrictions) { _, _ -> + fakeRestrictionsProvider + } + } + } + + private companion object { + const val TITLE = "Title" + const val USER_ID = 0 + const val RESTRICTION_KEY = "restriction_key" + } +} |