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

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

New RestrictedMainSwitchPreference

Restricted MainSwitchPreference.

Also move shared logic to RestrictedSwitchPreferenceModel.

Fix: 312048543
Test: unit tests
Change-Id: I84a104eda75e03570234a6e3424365eabefa8274
parent f23b8a96
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -45,7 +45,7 @@ import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel
import kotlinx.coroutines.flow.Flow

private const val ENTRY_NAME = "AppList"
@@ -157,7 +157,7 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>(
        }
        val restrictedMode by restrictionsProviderFactory.rememberRestrictedMode(restrictions)
        val allowed = listModel.isAllowed(record)
        return RestrictedSwitchPreference.getSummary(
        return RestrictedSwitchPreferenceModel.getSummary(
            context = context,
            restrictedModeSupplier = { restrictedMode },
            summaryIfNoRestricted = { getSummaryIfNoRestricted(allowed()) },
+47 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import com.android.settingslib.spa.widget.preference.MainSwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel.Companion.RestrictedSwitchWrapper

@Composable
fun RestrictedMainSwitchPreference(model: SwitchPreferenceModel, restrictions: Restrictions) {
    RestrictedMainSwitchPreference(model, restrictions, ::RestrictionsProviderImpl)
}

@VisibleForTesting
@Composable
internal fun RestrictedMainSwitchPreference(
    model: SwitchPreferenceModel,
    restrictions: Restrictions,
    restrictionsProviderFactory: RestrictionsProviderFactory,
) {
    if (restrictions.keys.isEmpty()) {
        MainSwitchPreference(model)
        return
    }
    restrictionsProviderFactory.RestrictedSwitchWrapper(model, restrictions) {
        MainSwitchPreference(it)
    }
}
+3 −102
Original line number Diff line number Diff line
@@ -16,29 +16,14 @@

package com.android.settingslib.spaprivileged.template.preference

import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.state.ToggleableState
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder
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.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel.Companion.RestrictedSwitchWrapper

@Composable
fun RestrictedSwitchPreference(
@@ -59,91 +44,7 @@ internal fun RestrictedSwitchPreference(
        SwitchPreference(model)
        return
    }
    val context = LocalContext.current
    val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value
    val restrictedSwitchModel = remember(restrictedMode) {
        RestrictedSwitchPreferenceModel(context, model, restrictedMode)
    }
    restrictedSwitchModel.RestrictionWrapper {
        SwitchPreference(restrictedSwitchModel)
    }
}

internal object RestrictedSwitchPreference {
    fun getSummary(
        context: Context,
        restrictedModeSupplier: () -> RestrictedMode?,
        summaryIfNoRestricted: () -> String,
        checked: () -> Boolean?,
    ): () -> String = {
        when (val restrictedMode = restrictedModeSupplier()) {
            is NoRestricted -> summaryIfNoRestricted()
            is BaseUserRestricted -> context.getString(com.android.settingslib.R.string.disabled)
            is BlockedByAdmin -> restrictedMode.getSummary(checked())
            null -> context.getPlaceholder()
        }
    }
}

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

    override val summary = RestrictedSwitchPreference.getSummary(
        context = context,
        restrictedModeSupplier = { restrictedMode },
        summaryIfNoRestricted = model.summary,
        checked = model.checked,
    )

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

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

    override val onCheckedChange = when (restrictedMode) {
        null -> null
        is NoRestricted -> model.onCheckedChange
        // Need to passthrough onCheckedChange for toggleable semantics, although since changeable
        // is false so this will not be called.
        is BaseUserRestricted -> model.onCheckedChange
        // Pass null since semantics ToggleableState is provided in RestrictionWrapper.
        is BlockedByAdmin -> null
    }

    @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())
                },
        ) { content() }
    }

    private fun ToggleableState(value: Boolean?) = when (value) {
        true -> ToggleableState.On
        false -> ToggleableState.Off
        null -> ToggleableState.Indeterminate
    restrictionsProviderFactory.RestrictedSwitchWrapper(model, restrictions) {
        SwitchPreference(it)
    }
}
+130 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.state.ToggleableState
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder
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.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode

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

    override val summary = getSummary(
        context = context,
        restrictedModeSupplier = { restrictedMode },
        summaryIfNoRestricted = model.summary,
        checked = model.checked,
    )

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

    override val changeable = if (restrictedMode is NoRestricted) model.changeable else ({ false })

    override val onCheckedChange = when (restrictedMode) {
        null -> null
        is NoRestricted -> model.onCheckedChange
        // Need to passthrough onCheckedChange for toggleable semantics, although since changeable
        // is false so this will not be called.
        is BaseUserRestricted -> model.onCheckedChange
        // Pass null since semantics ToggleableState is provided in RestrictionWrapper.
        is BlockedByAdmin -> null
    }

    @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())
                },
        ) { content() }
    }

    private fun ToggleableState(value: Boolean?) = when (value) {
        true -> ToggleableState.On
        false -> ToggleableState.Off
        null -> ToggleableState.Indeterminate
    }

    companion object {
        @Composable
        fun RestrictionsProviderFactory.RestrictedSwitchWrapper(
            model: SwitchPreferenceModel,
            restrictions: Restrictions,
            content: @Composable (SwitchPreferenceModel) -> Unit,
        ) {
            val context = LocalContext.current
            val restrictedMode = rememberRestrictedMode(restrictions).value
            val restrictedSwitchPreferenceModel = remember(restrictedMode) {
                RestrictedSwitchPreferenceModel(context, model, restrictedMode)
            }
            restrictedSwitchPreferenceModel.RestrictionWrapper {
                content(restrictedSwitchPreferenceModel)
            }
        }

        fun getSummary(
            context: Context,
            restrictedModeSupplier: () -> RestrictedMode?,
            summaryIfNoRestricted: () -> String,
            checked: () -> Boolean?,
        ): () -> String = {
            when (val restrictedMode = restrictedModeSupplier()) {
                is NoRestricted -> summaryIfNoRestricted()
                is BaseUserRestricted ->
                    context.getString(com.android.settingslib.R.string.disabled)

                is BlockedByAdmin -> restrictedMode.getSummary(checked())
                null -> context.getPlaceholder()
            }
        }
    }
}
+157 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.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.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.tests.testutils.FakeBlockedByAdmin
import com.android.settingslib.spaprivileged.tests.testutils.FakeRestrictionsProvider
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class RestrictedMainSwitchPreferenceTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    private val fakeBlockedByAdmin = FakeBlockedByAdmin()

    private val fakeRestrictionsProvider = FakeRestrictionsProvider()

    private val switchPreferenceModel = object : SwitchPreferenceModel {
        override val title = TITLE
        private val checkedState = mutableStateOf(true)
        override val checked = { checkedState.value }
        override val onCheckedChange: (Boolean) -> Unit = { checkedState.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(FakeBlockedByAdmin.SUMMARY).assertDoesNotExist()
        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 {
            RestrictedMainSwitchPreference(switchPreferenceModel, restrictions) { _, _ ->
                fakeRestrictionsProvider
            }
        }
    }

    private companion object {
        const val TITLE = "Title"
        const val USER_ID = 0
        const val RESTRICTION_KEY = "restriction_key"
    }
}