Merge "Add semantics for RestrictedSwitchPreference"
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
index eb7aaa7..3ea3b5c 100644
--- a/packages/SettingsLib/Spa/spa/Android.bp
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -33,6 +33,7 @@
"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",
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 b1adc9d..a618c3d 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
@@ -20,27 +20,41 @@
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
)
@@ -49,18 +63,31 @@
)
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) {
@@ -71,12 +98,7 @@
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
}
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 ec7d75e..6db2733 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
@@ -22,7 +22,6 @@
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
@@ -43,7 +42,7 @@
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
@@ -146,9 +145,7 @@
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> {
@@ -157,27 +154,27 @@
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)
}
}
}
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 31fd3ad..a003da8 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
@@ -22,12 +22,13 @@
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
@@ -38,32 +39,44 @@
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))
@@ -71,43 +84,64 @@
}
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
}
- else -> Modifier
+ Box(
+ Modifier
+ .clickable(
+ role = Role.Switch,
+ onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
+ )
+ .semantics {
+ this.toggleableState = ToggleableState(checked.value)
+ },
+ ) { content() }
+ }
+
+ private fun ToggleableState(value: Boolean?) = when (value) {
+ true -> ToggleableState.On
+ false -> ToggleableState.Off
+ null -> ToggleableState.Indeterminate
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt
new file mode 100644
index 0000000..a5352b2
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceTest.kt
@@ -0,0 +1,175 @@
+/*
+ * 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)
+}