Add semantics for RestrictedSwitchPreference
Add ToggleableState when BaseUserRestricted or BlockedByAdmin. Bug: 235727273 Test: Unit test Test: Manually when Talkback is on Change-Id: Iac68c0c726add6a1e57910f0953dde7805d23e71
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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) {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user