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

Commit f8685cc7 authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

Merge "Add semantics for RestrictedSwitchPreference"

parents bbbb9431 daa98eab
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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",
+42 −20
Original line number Diff line number Diff line
@@ -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
    }
+7 −10
Original line number Diff line number Diff line
@@ -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)
        }
    }
}
+52 −18
Original line number Diff line number Diff line
@@ -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
        }
        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 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)
}