From b3c8c71fb3218ca0de942748d4cef4d743903642 Mon Sep 17 00:00:00 2001 From: Yvonne Jiang Date: Tue, 21 Jan 2025 03:12:44 -0800 Subject: [PATCH] Initial skeleton of new "Supervision" settings screen with top-level entry point. Intake bug: b/379312924 Test: atest SupervisionDashboardScreenTest Test: atest SupervisionMainSwitchPreferenceTest Test: manually on real device Bug: 383568136 Change-Id: I6bb8aa432c1b4527cec0f4c2593fd1494975503d Flag: android.app.supervision.flags.enable_supervision_settings_screen --- res/drawable/ic_account_child_invert.xml | 25 +++++++ res/values/menu_keys.xml | 1 + res/values/strings.xml | 13 ++++ res/xml/top_level_settings_v2.xml | 10 +++ src/com/android/settings/supervision/OWNERS | 5 ++ .../SupervisionDashboardFragment.kt | 46 ++++++++++++ .../supervision/SupervisionDashboardScreen.kt | 68 ++++++++++++++++++ .../SupervisionMainSwitchPreference.kt | 71 +++++++++++++++++++ .../supervision/TitlelessPreferenceGroup.kt | 35 +++++++++ ...TopLevelSupervisionPreferenceController.kt | 28 ++++++++ .../SupervisionDashboardScreenTest.kt | 54 ++++++++++++++ .../SupervisionMainSwitchPreferenceTest.kt | 67 +++++++++++++++++ 12 files changed, 423 insertions(+) create mode 100644 res/drawable/ic_account_child_invert.xml create mode 100644 src/com/android/settings/supervision/OWNERS create mode 100644 src/com/android/settings/supervision/SupervisionDashboardFragment.kt create mode 100644 src/com/android/settings/supervision/SupervisionDashboardScreen.kt create mode 100644 src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt create mode 100644 src/com/android/settings/supervision/TitlelessPreferenceGroup.kt create mode 100644 src/com/android/settings/supervision/TopLevelSupervisionPreferenceController.kt create mode 100644 tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt create mode 100644 tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt diff --git a/res/drawable/ic_account_child_invert.xml b/res/drawable/ic_account_child_invert.xml new file mode 100644 index 00000000000..b6a799abb37 --- /dev/null +++ b/res/drawable/ic_account_child_invert.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/res/values/menu_keys.xml b/res/values/menu_keys.xml index 36fdb818c6c..5a589e466bf 100755 --- a/res/values/menu_keys.xml +++ b/res/values/menu_keys.xml @@ -37,5 +37,6 @@ top_level_system top_level_about_device top_level_support + top_level_supervision diff --git a/res/values/strings.xml b/res/values/strings.xml index f92ecc8c832..3ac0a600d94 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -14075,4 +14075,17 @@ %1$s. Configure device detail. + + + + + Supervision + + Content restrictions & other limits + + Use device supervision + + Set up PIN to get started + + supervision, parental supervision, parental controls diff --git a/res/xml/top_level_settings_v2.xml b/res/xml/top_level_settings_v2.xml index 4bc66f66e2c..5caae07dc4b 100644 --- a/res/xml/top_level_settings_v2.xml +++ b/res/xml/top_level_settings_v2.xml @@ -220,6 +220,16 @@ settings:highlightableMenuKey="@string/menu_key_accounts" settings:controller="com.android.settings.accounts.TopLevelAccountEntryPreferenceController"/> + + Supervision). + * + * See [SupervisionDashboardScreen] for details on the page contents. + * + * This class extends [DashboardFragment] in order to support dynamic settings injection. + */ +class SupervisionDashboardFragment : DashboardFragment() { + + override fun getPreferenceScreenResId() = R.xml.placeholder_preference_screen + + override fun getMetricsCategory() = SettingsEnums.SUPERVISION_DASHBOARD + + override fun getLogTag() = TAG + + override fun getPreferenceScreenBindingKey(context: Context) = SupervisionDashboardScreen.KEY + + // TODO(b/383405598): redirect to Play Store if supervisor client is not + // fully present. + + companion object { + private const val TAG = "SupervisionDashboard" + } +} diff --git a/src/com/android/settings/supervision/SupervisionDashboardScreen.kt b/src/com/android/settings/supervision/SupervisionDashboardScreen.kt new file mode 100644 index 00000000000..f31b0bdfeaf --- /dev/null +++ b/src/com/android/settings/supervision/SupervisionDashboardScreen.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2025 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.settings.supervision + +import android.app.supervision.flags.Flags +import android.content.Context +import com.android.settings.R +import com.android.settingslib.metadata.ProvidePreferenceScreen +import com.android.settingslib.metadata.preferenceHierarchy +import com.android.settingslib.preference.PreferenceScreenCreator + +/** + * Supervision settings landing page (Settings > Supervision). + * + * This screen typically includes three parts: + * 1. Primary switch to toggle supervision on and off. + * 2. List of supervision features. Individual features like website filters or bedtime schedules + * will be listed in a group and link out to their own respective settings pages. Features + * implemented by supervision client apps can also be dynamically injected into this group. + * 3. Entry point to supervision PIN management settings page. + */ +@ProvidePreferenceScreen(SupervisionDashboardScreen.KEY) +class SupervisionDashboardScreen : PreferenceScreenCreator { + + override fun isFlagEnabled(context: Context) = Flags.enableSupervisionSettingsScreen() + + override val key: String + get() = KEY + + override val title: Int + get() = R.string.supervision_settings_title + + override val summary: Int + get() = R.string.supervision_settings_summary + + override val icon: Int + get() = R.drawable.ic_account_child_invert + + override val keywords: Int + get() = R.string.keywords_supervision_settings + + override fun fragmentClass() = SupervisionDashboardFragment::class.java + + override fun getPreferenceHierarchy(context: Context) = + preferenceHierarchy(context, this) { + +SupervisionMainSwitchPreference() + +TitlelessPreferenceGroup("supervision_features_group_1") += { + // Empty category for dynamic injection targeting. + } + } + + companion object { + const val KEY = "top_level_supervision" + } +} diff --git a/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt b/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt new file mode 100644 index 00000000000..e7d1d7f25b0 --- /dev/null +++ b/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025 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.settings.supervision + +import android.app.supervision.SupervisionManager +import android.content.Context +import com.android.settings.R +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.NoOpKeyedObservable +import com.android.settingslib.metadata.MainSwitchPreference +import com.android.settingslib.metadata.PreferenceSummaryProvider +import com.android.settingslib.metadata.ReadWritePermit +import com.android.settingslib.metadata.SensitivityLevel + +/** Main toggle to enable or disable device supervision. */ +class SupervisionMainSwitchPreference : + MainSwitchPreference(KEY, R.string.device_supervision_switch_title), PreferenceSummaryProvider { + + // TODO(b/383568136): Make presence of summary conditional on whether PIN + // has been set up before or not. + override fun getSummary(context: Context): CharSequence? = + context.getString(R.string.device_supervision_switch_no_pin_summary) + + override fun storage(context: Context): KeyValueStore = SupervisionMainSwitchStorage(context) + + override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = + ReadWritePermit.DISALLOW + + override fun getWritePermit( + context: Context, + value: Boolean?, + callingPid: Int, + callingUid: Int, + ) = ReadWritePermit.DISALLOW + + override val sensitivityLevel: Int + get() = SensitivityLevel.HIGH_SENSITIVITY + + // TODO(b/390505725): Listen for changes in supervision state. + @Suppress("UNCHECKED_CAST") + private class SupervisionMainSwitchStorage(private val context: Context) : + NoOpKeyedObservable(), KeyValueStore { + override fun contains(key: String) = key == KEY + + override fun getValue(key: String, valueType: Class) = + (context.getSystemService(SupervisionManager::class.java)?.isSupervisionEnabled() == + true) + as T + + override fun setValue(key: String, valueType: Class, value: T?) { + // TODO(b/383402852): implement handling of main toggle. + } + } + + companion object { + const val KEY = "device_supervision_switch" + } +} diff --git a/src/com/android/settings/supervision/TitlelessPreferenceGroup.kt b/src/com/android/settings/supervision/TitlelessPreferenceGroup.kt new file mode 100644 index 00000000000..b0f3208bd2b --- /dev/null +++ b/src/com/android/settings/supervision/TitlelessPreferenceGroup.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 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.settings.supervision + +import androidx.preference.Preference +import com.android.settingslib.metadata.PreferenceCategory +import com.android.settingslib.metadata.PreferenceMetadata +import com.android.settingslib.preference.PreferenceCategoryBinding +import com.android.settingslib.widget.theme.R + +/** + * A [PreferenceCategory] that does not have a title, and hides the space reserved for displaying + * the title label above the category. + */ +class TitlelessPreferenceGroup(override val key: String) : + PreferenceCategory(key, title = 0), PreferenceCategoryBinding { + + override fun bind(preference: Preference, metadata: PreferenceMetadata) { + preference.layoutResource = R.layout.settingslib_preference_category_no_title + super.bind(preference, metadata) + } +} diff --git a/src/com/android/settings/supervision/TopLevelSupervisionPreferenceController.kt b/src/com/android/settings/supervision/TopLevelSupervisionPreferenceController.kt new file mode 100644 index 00000000000..c9979d9bdbf --- /dev/null +++ b/src/com/android/settings/supervision/TopLevelSupervisionPreferenceController.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 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.settings.supervision + +import android.app.supervision.flags.Flags +import android.content.Context +import com.android.settings.core.BasePreferenceController + +/** Controller for the top level Supervision settings Preference item. */ +class TopLevelSupervisionPreferenceController(context: Context, key: String) : + BasePreferenceController(context, key) { + + override fun getAvailabilityStatus(): Int = + if (Flags.enableSupervisionSettingsScreen()) AVAILABLE else UNSUPPORTED_ON_DEVICE +} diff --git a/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt b/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt new file mode 100644 index 00000000000..eef9ea4bbdb --- /dev/null +++ b/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2025 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.settings.supervision + +import android.app.supervision.flags.Flags +import android.content.Context +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SupervisionDashboardScreenTest { + @get:Rule val setFlagsRule = SetFlagsRule() + + private val preferenceScreenCreator = SupervisionDashboardScreen() + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Test + fun key() { + assertThat(preferenceScreenCreator.key).isEqualTo(SupervisionDashboardScreen.KEY) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_SUPERVISION_SETTINGS_SCREEN) + fun flagEnabled() { + assertThat(preferenceScreenCreator.isFlagEnabled(context)).isTrue() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_SUPERVISION_SETTINGS_SCREEN) + fun flagDisabled() { + assertThat(preferenceScreenCreator.isFlagEnabled(context)).isFalse() + } +} diff --git a/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt b/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt new file mode 100644 index 00000000000..4a4acfea7b1 --- /dev/null +++ b/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025 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.settings.supervision + +import android.app.supervision.SupervisionManager +import android.content.Context +import android.content.ContextWrapper +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.preference.createAndBindWidget +import com.android.settingslib.widget.MainSwitchPreference +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class SupervisionMainSwitchPreferenceTest { + private val preference = SupervisionMainSwitchPreference() + + private val mockSupervisionManager = mock() + + private val appContext: Context = ApplicationProvider.getApplicationContext() + private val context = + object : ContextWrapper(appContext) { + override fun getSystemService(name: String): Any = + when (name) { + Context.SUPERVISION_SERVICE -> mockSupervisionManager + else -> super.getSystemService(name) + } + } + + @Test + fun checked_supervisionEnabled_returnTrue() { + setSupervisionEnabled(true) + + assertThat(getMainSwitchPreference().isChecked).isTrue() + } + + @Test + fun checked_supervisionDisabled_returnFalse() { + setSupervisionEnabled(false) + + assertThat(getMainSwitchPreference().isChecked).isFalse() + } + + private fun getMainSwitchPreference(): MainSwitchPreference = + preference.createAndBindWidget(context) + + private fun setSupervisionEnabled(enabled: Boolean) = + mockSupervisionManager.stub { on { isSupervisionEnabled } doReturn enabled } +}