Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit daa98eab authored by Chaohui Wang's avatar Chaohui Wang
Browse files

Add semantics for RestrictedSwitchPreference

Add ToggleableState when BaseUserRestricted or BlockedByAdmin.

Bug: 235727273
Test: Unit test
Test: Manually when Talkback is on
Change-Id: Iac68c0c726add6a1e57910f0953dde7805d23e71
parent 5cf37839
Loading
Loading
Loading
Loading
+1 −0
Original line number Original line Diff line number Diff line
@@ -33,6 +33,7 @@ android_library {
        "androidx.compose.runtime_runtime-livedata",
        "androidx.compose.runtime_runtime-livedata",
        "androidx.compose.ui_ui-tooling-preview",
        "androidx.compose.ui_ui-tooling-preview",
        "androidx.lifecycle_lifecycle-livedata-ktx",
        "androidx.lifecycle_lifecycle-livedata-ktx",
        "androidx.lifecycle_lifecycle-runtime-compose",
        "androidx.navigation_navigation-compose",
        "androidx.navigation_navigation-compose",
        "com.google.android.material_material",
        "com.google.android.material_material",
        "lottie_compose",
        "lottie_compose",
+42 −20
Original line number Original line Diff line number Diff line
@@ -20,27 +20,41 @@ import android.app.admin.DevicePolicyResources.Strings.Settings
import android.content.Context
import android.content.Context
import android.os.UserHandle
import android.os.UserHandle
import android.os.UserManager
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.RestrictedLockUtils.EnforcedAdmin
import com.android.settingslib.RestrictedLockUtilsInternal
import com.android.settingslib.RestrictedLockUtilsInternal
import com.android.settingslib.spaprivileged.R
import com.android.settingslib.spaprivileged.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn


data class Restrictions(
data class Restrictions(
    val userId: Int,
    val userId: Int,
    val keys: List<String>,
    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(
interface BlockedByAdmin : RestrictedMode {
    val enterpriseRepository: EnterpriseRepository,
    fun getSummary(checked: Boolean?): String
    val enforcedAdmin: EnforcedAdmin,
    fun sendShowAdminSupportDetailsIntent()
) : RestrictedMode() {
}
    fun getSummary(checked: Boolean?): String = when (checked) {

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(
        true -> enterpriseRepository.getEnterpriseString(
            Settings.ENABLED_BY_ADMIN_SWITCH_SUMMARY, R.string.enabled_by_admin
            Settings.ENABLED_BY_ADMIN_SWITCH_SUMMARY, R.string.enabled_by_admin
        )
        )
@@ -49,18 +63,31 @@ data class BlockedByAdmin(
        )
        )
        else -> ""
        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 context: Context,
    private val restrictions: Restrictions,
    private val restrictions: Restrictions,
) {
) : RestrictionsProvider {
    private val userManager by lazy { UserManager.get(context) }
    private val userManager by lazy { UserManager.get(context) }
    private val enterpriseRepository by lazy { EnterpriseRepository(context) }


    val restrictedMode = liveData {
    private val restrictedMode = flow {
        emit(getRestrictedMode())
        emit(getRestrictedMode())
    }
    }.flowOn(Dispatchers.IO)

    @OptIn(ExperimentalLifecycleComposeApi::class)
    @Composable
    override fun restrictedModeState() =
        restrictedMode.collectAsStateWithLifecycle(initialValue = null)


    private fun getRestrictedMode(): RestrictedMode {
    private fun getRestrictedMode(): RestrictedMode {
        for (key in restrictions.keys) {
        for (key in restrictions.keys) {
@@ -71,12 +98,7 @@ class RestrictionsProvider(
        for (key in restrictions.keys) {
        for (key in restrictions.keys) {
            RestrictedLockUtilsInternal
            RestrictedLockUtilsInternal
                .checkIfRestrictionEnforced(context, key, restrictions.userId)
                .checkIfRestrictionEnforced(context, key, restrictions.userId)
                ?.let {
                ?.let { return BlockedByAdminImpl(context = context, enforcedAdmin = it) }
                    return BlockedByAdmin(
                        enterpriseRepository = enterpriseRepository,
                        enforcedAdmin = it,
                    )
                }
        }
        }
        return NoRestricted
        return NoRestricted
    }
    }
+7 −10
Original line number Original line Diff line number Diff line
@@ -22,7 +22,6 @@ import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.AppRecord
import com.android.settingslib.spaprivileged.model.app.userId
import com.android.settingslib.spaprivileged.model.app.userId
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
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 com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow


@@ -146,9 +145,7 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>(
        listModel.filter(userIdFlow, recordListFlow)
        listModel.filter(userIdFlow, recordListFlow)


    @Composable
    @Composable
    override fun getSummary(option: Int, record: T): State<String> {
    override fun getSummary(option: Int, record: T) = getSummary(record)
        return getSummary(record)
    }


    @Composable
    @Composable
    fun getSummary(record: T): State<String> {
    fun getSummary(record: T): State<String> {
@@ -157,27 +154,27 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>(
                userId = record.app.userId,
                userId = record.app.userId,
                keys = listModel.switchRestrictionKeys,
                keys = listModel.switchRestrictionKeys,
            )
            )
            RestrictionsProvider(context, restrictions)
            RestrictionsProviderImpl(context, restrictions)
        }
        }
        val restrictedMode = restrictionsProvider.restrictedMode.observeAsState()
        val restrictedMode = restrictionsProvider.restrictedModeState()
        val allowed = listModel.isAllowed(record)
        val allowed = listModel.isAllowed(record)
        return remember {
        return remember {
            derivedStateOf {
            derivedStateOf {
                RestrictedSwitchPreference.getSummary(
                RestrictedSwitchPreference.getSummary(
                    context = context,
                    context = context,
                    restrictedMode = restrictedMode.value,
                    restrictedMode = restrictedMode.value,
                    noRestrictedSummary = getNoRestrictedSummary(allowed),
                    summaryIfNoRestricted = getSummaryIfNoRestricted(allowed),
                    checked = allowed,
                    checked = allowed,
                ).value
                ).value
            }
            }
        }
        }
    }
    }


    private fun getNoRestrictedSummary(allowed: State<Boolean?>) = derivedStateOf {
    private fun getSummaryIfNoRestricted(allowed: State<Boolean?>) = derivedStateOf {
        when (allowed.value) {
        when (allowed.value) {
            true -> context.getString(R.string.app_permission_summary_allowed)
            true -> context.getString(R.string.app_permission_summary_allowed)
            false -> context.getString(R.string.app_permission_summary_not_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)
        }
        }
    }
    }
}
}
+52 −18
Original line number Original line Diff line number Diff line
@@ -22,12 +22,13 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
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.framework.compose.stateOf
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
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.RestrictedMode
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl


@Composable
@Composable
fun RestrictedSwitchPreference(model: SwitchPreferenceModel, restrictions: Restrictions) {
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()) {
    if (restrictions.keys.isEmpty()) {
        SwitchPreference(model)
        SwitchPreference(model)
        return
        return
    }
    }
    val context = LocalContext.current
    val context = LocalContext.current
    val restrictionsProvider = remember { RestrictionsProvider(context, restrictions) }
    val restrictionsProvider = remember(restrictions) {
    val restrictedMode = restrictionsProvider.restrictedMode.observeAsState().value ?: return
        restrictionsProviderFactory(context, restrictions)
    }
    val restrictedMode = restrictionsProvider.restrictedModeState().value
    val restrictedSwitchModel = remember(restrictedMode) {
    val restrictedSwitchModel = remember(restrictedMode) {
        RestrictedSwitchPreferenceModel(context, model, restrictedMode)
        RestrictedSwitchPreferenceModel(context, model, restrictedMode)
    }
    }
    Box(remember { restrictedSwitchModel.getModifier() }) {
    restrictedSwitchModel.RestrictionWrapper {
        SwitchPreference(restrictedSwitchModel)
        SwitchPreference(restrictedSwitchModel)
    }
    }
}
}


object RestrictedSwitchPreference {
internal object RestrictedSwitchPreference {
    fun getSummary(
    fun getSummary(
        context: Context,
        context: Context,
        restrictedMode: RestrictedMode?,
        restrictedMode: RestrictedMode?,
        noRestrictedSummary: State<String>,
        summaryIfNoRestricted: State<String>,
        checked: State<Boolean?>,
        checked: State<Boolean?>,
    ): State<String> = when (restrictedMode) {
    ): State<String> = when (restrictedMode) {
        is NoRestricted -> noRestrictedSummary
        is NoRestricted -> summaryIfNoRestricted
        is BaseUserRestricted -> stateOf(context.getString(R.string.disabled))
        is BaseUserRestricted -> stateOf(context.getString(R.string.disabled))
        is BlockedByAdmin -> derivedStateOf { restrictedMode.getSummary(checked.value) }
        is BlockedByAdmin -> derivedStateOf { restrictedMode.getSummary(checked.value) }
        null -> stateOf(context.getString(R.string.summary_placeholder))
        null -> stateOf(context.getString(R.string.summary_placeholder))
@@ -71,43 +84,64 @@ object RestrictedSwitchPreference {
}
}


private class RestrictedSwitchPreferenceModel(
private class RestrictedSwitchPreferenceModel(
    private val context: Context,
    context: Context,
    model: SwitchPreferenceModel,
    model: SwitchPreferenceModel,
    private val restrictedMode: RestrictedMode,
    private val restrictedMode: RestrictedMode?,
) : SwitchPreferenceModel {
) : SwitchPreferenceModel {
    override val title = model.title
    override val title = model.title


    override val summary = RestrictedSwitchPreference.getSummary(
    override val summary = RestrictedSwitchPreference.getSummary(
        context = context,
        context = context,
        restrictedMode = restrictedMode,
        restrictedMode = restrictedMode,
        noRestrictedSummary = model.summary,
        summaryIfNoRestricted = model.summary,
        checked = model.checked,
        checked = model.checked,
    )
    )


    override val checked = when (restrictedMode) {
    override val checked = when (restrictedMode) {
        null -> stateOf(null)
        is NoRestricted -> model.checked
        is NoRestricted -> model.checked
        is BaseUserRestricted -> stateOf(false)
        is BaseUserRestricted -> stateOf(false)
        is BlockedByAdmin -> model.checked
        is BlockedByAdmin -> model.checked
    }
    }


    override val changeable = when (restrictedMode) {
    override val changeable = when (restrictedMode) {
        null -> stateOf(false)
        is NoRestricted -> model.changeable
        is NoRestricted -> model.changeable
        is BaseUserRestricted -> stateOf(false)
        is BaseUserRestricted -> stateOf(false)
        is BlockedByAdmin -> stateOf(false)
        is BlockedByAdmin -> stateOf(false)
    }
    }


    override val onCheckedChange = when (restrictedMode) {
    override val onCheckedChange = when (restrictedMode) {
        null -> null
        is NoRestricted -> model.onCheckedChange
        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
        is BlockedByAdmin -> null
    }
    }


    fun getModifier(): Modifier = when (restrictedMode) {
    @Composable
        is BlockedByAdmin -> Modifier.clickable(role = Role.Switch) {
    fun RestrictionWrapper(content: @Composable () -> Unit) {
            RestrictedLockUtils.sendShowAdminSupportDetailsIntent(
        if (restrictedMode !is BlockedByAdmin) {
                context, restrictedMode.enforcedAdmin
            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
    }
    }
}
}
+175 −0
Original line number Original line 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)
}