Merge "Add semantics for RestrictedSwitchPreference"

This commit is contained in:
Chaohui Wang
2022-11-26 05:16:41 +00:00
committed by Android (Google) Code Review
5 changed files with 277 additions and 48 deletions

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
}