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
This commit is contained in:
Yvonne Jiang
2025-01-21 03:12:44 -08:00
parent 968807c3bc
commit b3c8c71fb3
12 changed files with 423 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,80Q530,80 565,115Q600,150 600,200Q600,250 565,285Q530,320 480,320Q430,320 395,285Q360,250 360,200Q360,150 395,115Q430,80 480,80ZM480,360Q527,360 573,371Q619,382 656,402Q694,421 717,447Q740,473 740,504L740,736Q740,753 732,769.5Q724,786 710,800Q696,814 677.5,826Q659,838 636,848L636,758Q636,720 583.5,696Q531,672 480,672Q430,672 383.5,692.5Q337,713 326,746Q364,761 404,767Q444,773 486,774Q494,774 502.5,774Q511,774 520,774L520,878Q513,880 505.5,880Q498,880 490,880Q454,880 407.5,872Q361,864 319,847Q277,830 248.5,802.5Q220,775 220,736L220,504Q220,473 243,447Q266,421 303,402Q341,382 387,371Q433,360 480,360ZM480,600Q513,600 536.5,576.5Q560,553 560,520Q560,487 536.5,463.5Q513,440 480,440Q447,440 423.5,463.5Q400,487 400,520Q400,553 423.5,576.5Q447,600 480,600Z"/>
</vector>

View File

@@ -37,5 +37,6 @@
<string name="menu_key_system" translatable="false">top_level_system</string> <string name="menu_key_system" translatable="false">top_level_system</string>
<string name="menu_key_about_device" translatable="false">top_level_about_device</string> <string name="menu_key_about_device" translatable="false">top_level_about_device</string>
<string name="menu_key_support" translatable="false">top_level_support</string> <string name="menu_key_support" translatable="false">top_level_support</string>
<string name="menu_key_supervision" translatable="false">top_level_supervision</string>
</resources> </resources>

View File

@@ -14075,4 +14075,17 @@
<!-- Content description for device details icon on Connected device page [CHAR LIMIT=NONE] --> <!-- Content description for device details icon on Connected device page [CHAR LIMIT=NONE] -->
<string name="device_detail_icon_content_description"><xliff:g id="device_name">%1$s</xliff:g>. Configure device detail.</string> <string name="device_detail_icon_content_description"><xliff:g id="device_name">%1$s</xliff:g>. Configure device detail.</string>
<!-- Supervision settings -->
<!-- Title for Supervision settings menu entry [CHAR LIMIT=60] -->
<string name="supervision_settings_title">Supervision</string>
<!-- Description for Supervision settings menu entry [CHAR LIMIT=NONE] -->
<string name="supervision_settings_summary">Content restrictions &amp; other limits</string>
<!-- Title of toggle to enable or disable device supervision [CHAR LIMIT=60] -->
<string name="device_supervision_switch_title">Use device supervision</string>
<!-- Description of device supervision toggle when PIN has never been set up [CHAR LIMIT=NONE] -->
<string name="device_supervision_switch_no_pin_summary">Set up PIN to get started</string>
<!-- Search keywords for supervision settings [CHAR LIMIT=NONE] -->
<string name="keywords_supervision_settings">supervision, parental supervision, parental controls</string>
</resources> </resources>

View File

@@ -220,6 +220,16 @@
settings:highlightableMenuKey="@string/menu_key_accounts" settings:highlightableMenuKey="@string/menu_key_accounts"
settings:controller="com.android.settings.accounts.TopLevelAccountEntryPreferenceController"/> settings:controller="com.android.settings.accounts.TopLevelAccountEntryPreferenceController"/>
<com.android.settings.widget.HomepagePreference
android:key="top_level_supervision"
android:title="@string/supervision_settings_title"
android:summary="@string/supervision_settings_summary"
android:icon="@drawable/ic_account_child_invert"
android:order="5"
android:fragment="com.android.settings.supervision.SupervisionDashboardFragment"
settings:highlightableMenuKey="@string/menu_key_supervision"
settings:controller="com.android.settings.supervision.TopLevelSupervisionPreferenceController"/>
<com.android.settings.widget.HomepagePreference <com.android.settings.widget.HomepagePreference
android:key="top_level_emergency" android:key="top_level_emergency"
android:title="@string/emergency_settings_preference_title" android:title="@string/emergency_settings_preference_title"

View File

@@ -0,0 +1,5 @@
# Bug component: 1680938
jparks@google.com
sonnyf@google.com
yvonnejiang@google.com

View File

@@ -0,0 +1,46 @@
/*
* 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.settings.SettingsEnums
import android.content.Context
import com.android.settings.R
import com.android.settings.dashboard.DashboardFragment
/**
* Fragment to display the Supervision settings landing page (Settings > 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"
}
}

View File

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

View File

@@ -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<String>(), KeyValueStore {
override fun contains(key: String) = key == KEY
override fun <T : Any> getValue(key: String, valueType: Class<T>) =
(context.getSystemService(SupervisionManager::class.java)?.isSupervisionEnabled() ==
true)
as T
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) {
// TODO(b/383402852): implement handling of main toggle.
}
}
companion object {
const val KEY = "device_supervision_switch"
}
}

View File

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

View File

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

View File

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

View File

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