[Device Supervision] Implement createConfirmSupervisionCredentialsIntent API

The `ConfirmSupervisionCredentialsActivity` has been added and it's intended to be launched via the intent.

Bug: 392961554
Flag: android.app.supervision.flags.enable_supervision_settings_screen
Test: atest SupervisionMainSwitchPreferenceTest
Change-Id: I2322256a5711d5b90f826f467110c6861a7734ad
This commit is contained in:
juquan
2025-02-13 07:11:54 +00:00
committed by Junchen Quan
parent 04cfbe0520
commit 160b8bc1bb
8 changed files with 278 additions and 27 deletions

View File

@@ -139,6 +139,7 @@ android_library {
"aconfig_settings_flags", "aconfig_settings_flags",
"aconfig_settingslib_flags", "aconfig_settingslib_flags",
"android.app.flags-aconfig", "android.app.flags-aconfig",
"android.app.supervision.flags-aconfig",
"android.provider.flags-aconfig", "android.provider.flags-aconfig",
"android.security.flags-aconfig", "android.security.flags-aconfig",
"android.view.contentprotection.flags-aconfig", "android.view.contentprotection.flags-aconfig",

View File

@@ -2817,6 +2817,15 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".supervision.ConfirmSupervisionCredentialsActivity"
android:exported="true"
android:featureFlag="android.app.supervision.flags.supervision_manager_apis">
<intent-filter>
<action android:name="android.app.supervision.action.CONFIRM_SUPERVISION_CREDENTIALS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".SetupRedactionInterstitial" <activity android:name=".SetupRedactionInterstitial"
android:enabled="false" android:enabled="false"
android:exported="true" android:exported="true"

View File

@@ -14309,5 +14309,7 @@ Data usage charges may apply.</string>
<!-- Title for web content filters browser category allow all sites option [CHAR LIMIT=60] --> <!-- Title for web content filters browser category allow all sites option [CHAR LIMIT=60] -->
<string name="supervision_web_content_filters_browser_allow_all_sites_title">Allow all sites</string> <string name="supervision_web_content_filters_browser_allow_all_sites_title">Allow all sites</string>
<!-- Generic content description that is attached to the preview illustration at the top of an Accessibility feature toggle page. [CHAR LIMIT=NONE] --> <!-- Generic content description that is attached to the preview illustration at the top of an Accessibility feature toggle page. [CHAR LIMIT=NONE] -->
<!-- Title for supervision PIN verification screen [CHAR LIMIT=60] -->
<string name="supervision_full_screen_pin_verification_title">Enter supervision PIN</string>
<string name="accessibility_illustration_content_description"><xliff:g id="feature" example="Select to Speak">%1$s</xliff:g> animation</string> <string name="accessibility_illustration_content_description"><xliff:g id="feature" example="Select to Speak">%1$s</xliff:g> animation</string>
</resources> </resources>

View File

@@ -0,0 +1,101 @@
/*
* 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.Manifest.permission.USE_BIOMETRIC
import android.app.Activity
import android.content.pm.PackageManager
import android.hardware.biometrics.BiometricManager
import android.hardware.biometrics.BiometricPrompt
import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
import android.os.Bundle
import android.os.CancellationSignal
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.android.settings.R
/**
* Activity for confirming supervision credentials using device credential authentication.
*
* This activity displays an authentication prompt to the user, requiring them to authenticate using
* their device credentials (PIN, pattern, or password). It is specifically designed for verifying
* credentials for supervision purposes.
*
* It returns `Activity.RESULT_OK` if authentication succeeds, and `Activity.RESULT_CANCELED` if
* authentication fails or is canceled by the user.
*
* Usage:
* 1. Start this activity using `startActivityForResult()`.
* 2. Handle the result in `onActivityResult()`.
*
* Permissions:
* - Requires `android.permission.USE_BIOMETRIC`.
*/
class ConfirmSupervisionCredentialsActivity : FragmentActivity() {
private val mAuthenticationCallback =
object : AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
Log.w(TAG, "onAuthenticationError(errorCode=$errorCode, errString=$errString)")
setResult(Activity.RESULT_CANCELED)
finish()
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
setResult(Activity.RESULT_OK)
finish()
}
override fun onAuthenticationFailed() {
setResult(Activity.RESULT_CANCELED)
finish()
}
}
@RequiresPermission(USE_BIOMETRIC)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// TODO(b/392961554): Check if caller is the SYSTEM_SUPERVISION role holder. Call
// RoleManager#getRoleHolders(SYSTEM_SUPERVISION) and check if getCallingPackage() is in the
// list.
if (checkCallingOrSelfPermission(USE_BIOMETRIC) == PackageManager.PERMISSION_GRANTED) {
showBiometricPrompt()
}
}
@RequiresPermission(USE_BIOMETRIC)
fun showBiometricPrompt() {
// TODO(b/392961554): adapts to new user profile type to trigger PIN verification dialog.
val biometricPrompt =
BiometricPrompt.Builder(this)
.setTitle(getString(R.string.supervision_full_screen_pin_verification_title))
.setConfirmationRequired(true)
.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.build()
biometricPrompt.authenticate(
CancellationSignal(),
ContextCompat.getMainExecutor(this),
mAuthenticationCallback,
)
}
companion object {
// TODO(b/392961554): remove this tag and use shared tag after http://ag/31997167 is
// submitted.
const val TAG = "SupervisionSettings"
}
}

View File

@@ -56,7 +56,7 @@ class SupervisionDashboardScreen : PreferenceScreenCreator {
override fun getPreferenceHierarchy(context: Context) = override fun getPreferenceHierarchy(context: Context) =
preferenceHierarchy(context, this) { preferenceHierarchy(context, this) {
+SupervisionMainSwitchPreference() +SupervisionMainSwitchPreference(context)
+TitlelessPreferenceGroup(SUPERVISION_DYNAMIC_GROUP_1) += { +TitlelessPreferenceGroup(SUPERVISION_DYNAMIC_GROUP_1) += {
+SupervisionWebContentFiltersScreen.KEY +SupervisionWebContentFiltersScreen.KEY
} }

View File

@@ -15,8 +15,10 @@
*/ */
package com.android.settings.supervision package com.android.settings.supervision
import android.app.Activity
import android.app.supervision.SupervisionManager import android.app.supervision.SupervisionManager
import android.content.Context import android.content.Context
import android.content.Intent
import androidx.preference.Preference import androidx.preference.Preference
import com.android.settings.R import com.android.settings.R
import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyValueStore
@@ -32,19 +34,22 @@ import com.android.settingslib.preference.MainSwitchPreferenceBinding
import com.android.settingslib.preference.forEachRecursively import com.android.settingslib.preference.forEachRecursively
/** Main toggle to enable or disable device supervision. */ /** Main toggle to enable or disable device supervision. */
class SupervisionMainSwitchPreference : class SupervisionMainSwitchPreference(context: Context) :
MainSwitchPreference(KEY, R.string.device_supervision_switch_title), MainSwitchPreference(KEY, R.string.device_supervision_switch_title),
PreferenceSummaryProvider, PreferenceSummaryProvider,
MainSwitchPreferenceBinding, MainSwitchPreferenceBinding,
Preference.OnPreferenceChangeListener, Preference.OnPreferenceChangeListener,
PreferenceLifecycleProvider { PreferenceLifecycleProvider {
private val supervisionMainSwitchStorage = SupervisionMainSwitchStorage(context)
private lateinit var lifeCycleContext: PreferenceLifecycleContext
// TODO(b/383568136): Make presence of summary conditional on whether PIN // TODO(b/383568136): Make presence of summary conditional on whether PIN
// has been set up before or not. // has been set up before or not.
override fun getSummary(context: Context): CharSequence? = override fun getSummary(context: Context): CharSequence? =
context.getString(R.string.device_supervision_switch_no_pin_summary) context.getString(R.string.device_supervision_switch_no_pin_summary)
override fun storage(context: Context): KeyValueStore = SupervisionMainSwitchStorage(context) override fun storage(context: Context): KeyValueStore = supervisionMainSwitchStorage
override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) =
ReadWritePermit.DISALLOW ReadWritePermit.DISALLOW
@@ -55,26 +60,49 @@ class SupervisionMainSwitchPreference :
override val sensitivityLevel: Int override val sensitivityLevel: Int
get() = SensitivityLevel.HIGH_SENSITIVITY get() = SensitivityLevel.HIGH_SENSITIVITY
override fun onCreate(context: PreferenceLifecycleContext) {
lifeCycleContext = context
}
override fun onResume(context: PreferenceLifecycleContext) {
updateDependentPreferencesEnabledState(
context.findPreference<Preference>(KEY),
supervisionMainSwitchStorage.getBoolean(KEY)!!,
)
}
override fun onActivityResult(
context: PreferenceLifecycleContext,
requestCode: Int,
resultCode: Int,
data: Intent?,
): Boolean {
if (resultCode == Activity.RESULT_OK) {
val mainSwitchPreference =
context.requirePreference<com.android.settingslib.widget.MainSwitchPreference>(KEY)
val newValue = !supervisionMainSwitchStorage.getBoolean(KEY)!!
mainSwitchPreference.setChecked(newValue)
updateDependentPreferencesEnabledState(mainSwitchPreference, newValue)
}
return true
}
override fun bind(preference: Preference, metadata: PreferenceMetadata) { override fun bind(preference: Preference, metadata: PreferenceMetadata) {
super.bind(preference, metadata) super.bind(preference, metadata)
preference.onPreferenceChangeListener = this preference.onPreferenceChangeListener = this
} }
override fun onResume(context: PreferenceLifecycleContext) {
val currentValue = storage(context.applicationContext)?.getBoolean(key) ?: false
updateDependentPreferencesEnabledState(
context.findPreference<Preference>(KEY),
currentValue,
)
}
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
if (newValue !is Boolean) return true if (newValue !is Boolean) return true
updateDependentPreferencesEnabledState(preference, newValue) val intent = Intent(lifeCycleContext, ConfirmSupervisionCredentialsActivity::class.java)
lifeCycleContext.startActivityForResult(
return true intent,
REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS,
null,
)
return false
} }
private fun updateDependentPreferencesEnabledState( private fun updateDependentPreferencesEnabledState(
@@ -83,9 +111,8 @@ class SupervisionMainSwitchPreference :
) { ) {
preference?.parent?.forEachRecursively { preference?.parent?.forEachRecursively {
if ( if (
it.parent?.key?.toString() == it.parent?.key == SupervisionDashboardScreen.SUPERVISION_DYNAMIC_GROUP_1 ||
SupervisionDashboardScreen.SUPERVISION_DYNAMIC_GROUP_1 || it.key == SupervisionPinManagementScreen.KEY
it.key?.toString() == SupervisionPinManagementScreen.KEY
) { ) {
it.isEnabled = isChecked it.isEnabled = isChecked
} }
@@ -103,7 +130,6 @@ class SupervisionMainSwitchPreference :
as T as T
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) { override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) {
// TODO(b/392694561): add PIN protection to main toggle.
if (key == KEY && value is Boolean) { if (key == KEY && value is Boolean) {
val supervisionManager = context.getSystemService(SupervisionManager::class.java) val supervisionManager = context.getSystemService(SupervisionManager::class.java)
supervisionManager?.setSupervisionEnabled(value) supervisionManager?.setSupervisionEnabled(value)
@@ -113,5 +139,6 @@ class SupervisionMainSwitchPreference :
companion object { companion object {
const val KEY = "device_supervision_switch" const val KEY = "device_supervision_switch"
const val REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS = 0
} }
} }

View File

@@ -15,6 +15,7 @@
*/ */
package com.android.settings.supervision package com.android.settings.supervision
import android.app.Activity
import android.app.supervision.flags.Flags import android.app.supervision.flags.Flags
import android.content.Context import android.content.Context
import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.DisableFlags
@@ -24,6 +25,7 @@ import androidx.fragment.app.testing.FragmentScenario
import androidx.preference.Preference import androidx.preference.Preference
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.supervision.SupervisionMainSwitchPreference.Companion.REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS
import com.android.settingslib.widget.MainSwitchPreference import com.android.settingslib.widget.MainSwitchPreference
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Rule import org.junit.Rule
@@ -57,7 +59,7 @@ class SupervisionDashboardScreenTest {
@Test @Test
@EnableFlags(Flags.FLAG_ENABLE_SUPERVISION_SETTINGS_SCREEN) @EnableFlags(Flags.FLAG_ENABLE_SUPERVISION_SETTINGS_SCREEN)
fun toggleMainSwitch_disablesChildPreferences() { fun toggleMainSwitch_pinVerificationSucceeded_enablesChildPreferences() {
FragmentScenario.launchInContainer(preferenceScreenCreator.fragmentClass()).onFragment { FragmentScenario.launchInContainer(preferenceScreenCreator.fragmentClass()).onFragment {
fragment -> fragment ->
val mainSwitchPreference = val mainSwitchPreference =
@@ -68,8 +70,38 @@ class SupervisionDashboardScreenTest {
assertThat(childPreference.isEnabled).isFalse() assertThat(childPreference.isEnabled).isFalse()
mainSwitchPreference.performClick() mainSwitchPreference.performClick()
// Pretend the PIN verification succeeded.
fragment.onActivityResult(
requestCode = REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS,
resultCode = Activity.RESULT_OK,
data = null,
)
assertThat(childPreference.isEnabled).isTrue() assertThat(childPreference.isEnabled).isTrue()
} }
} }
@Test
@EnableFlags(Flags.FLAG_ENABLE_SUPERVISION_SETTINGS_SCREEN)
fun toggleMainSwitch_pinVerificationFailed_childPreferencesRemainDisabled() {
FragmentScenario.launchInContainer(preferenceScreenCreator.fragmentClass()).onFragment {
fragment ->
val mainSwitchPreference =
fragment.findPreference<MainSwitchPreference>(SupervisionMainSwitchPreference.KEY)!!
val childPreference =
fragment.findPreference<Preference>(SupervisionPinManagementScreen.KEY)!!
assertThat(childPreference.isEnabled).isFalse()
mainSwitchPreference.performClick()
// Pretend the PIN verification failed.
fragment.onActivityResult(
requestCode = REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS,
resultCode = Activity.RESULT_CANCELED,
data = null,
)
assertThat(childPreference.isEnabled).isFalse()
}
}
} }

View File

@@ -15,25 +15,33 @@
*/ */
package com.android.settings.supervision package com.android.settings.supervision
import android.app.Activity
import android.app.supervision.SupervisionManager import android.app.supervision.SupervisionManager
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent
import androidx.preference.Preference
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.supervision.SupervisionMainSwitchPreference.Companion.REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS
import com.android.settingslib.metadata.PreferenceLifecycleContext
import com.android.settingslib.preference.createAndBindWidget import com.android.settingslib.preference.createAndBindWidget
import com.android.settingslib.widget.MainSwitchPreference import com.android.settingslib.widget.MainSwitchPreference
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.stub import org.mockito.kotlin.stub
import org.mockito.kotlin.verify import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SupervisionMainSwitchPreferenceTest { class SupervisionMainSwitchPreferenceTest {
private val preference = SupervisionMainSwitchPreference() private val mockLifeCycleContext = mock<PreferenceLifecycleContext>()
private val mockSupervisionManager = mock<SupervisionManager>() private val mockSupervisionManager = mock<SupervisionManager>()
private val appContext: Context = ApplicationProvider.getApplicationContext() private val appContext: Context = ApplicationProvider.getApplicationContext()
@@ -46,6 +54,13 @@ class SupervisionMainSwitchPreferenceTest {
} }
} }
private val preference = SupervisionMainSwitchPreference(context)
@Before
fun setUp() {
preference.onCreate(mockLifeCycleContext)
}
@Test @Test
fun checked_supervisionEnabled_returnTrue() { fun checked_supervisionEnabled_returnTrue() {
setSupervisionEnabled(true) setSupervisionEnabled(true)
@@ -61,7 +76,7 @@ class SupervisionMainSwitchPreferenceTest {
} }
@Test @Test
fun toggleOn() { fun toggleOn_triggersPinVerification() {
setSupervisionEnabled(false) setSupervisionEnabled(false)
val widget = getMainSwitchPreference() val widget = getMainSwitchPreference()
@@ -69,26 +84,90 @@ class SupervisionMainSwitchPreferenceTest {
widget.performClick() widget.performClick()
verifyConfirmSupervisionCredentialsActivityStarted()
assertThat(widget.isChecked).isFalse()
verify(mockSupervisionManager, never()).setSupervisionEnabled(false)
}
@Test
fun toggleOn_pinVerificationSucceeded_supervisionEnabled() {
setSupervisionEnabled(false)
val widget = getMainSwitchPreference()
assertThat(widget.isChecked).isFalse()
preference.onActivityResult(
mockLifeCycleContext,
REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS,
Activity.RESULT_OK,
null,
)
assertThat(widget.isChecked).isTrue() assertThat(widget.isChecked).isTrue()
verify(mockSupervisionManager).setSupervisionEnabled(true) verify(mockSupervisionManager).setSupervisionEnabled(true)
} }
@Test @Test
fun toggleOff() { fun toggleOff_pinVerificationSucceeded_supervisionDisabled() {
setSupervisionEnabled(true) setSupervisionEnabled(true)
val widget = getMainSwitchPreference() val widget = getMainSwitchPreference()
assertThat(widget.isChecked).isTrue() assertThat(widget.isChecked).isTrue()
widget.performClick() preference.onActivityResult(
mockLifeCycleContext,
REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS,
Activity.RESULT_OK,
null,
)
assertThat(widget.isChecked).isFalse() assertThat(widget.isChecked).isFalse()
verify(mockSupervisionManager).setSupervisionEnabled(false) verify(mockSupervisionManager).setSupervisionEnabled(false)
} }
private fun getMainSwitchPreference(): MainSwitchPreference = @Test
preference.createAndBindWidget(context) fun toggleOff_pinVerificationFailed_supervisionNotEnabled() {
setSupervisionEnabled(true)
val widget = getMainSwitchPreference()
assertThat(widget.isChecked).isTrue()
preference.onActivityResult(
mockLifeCycleContext,
REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS,
Activity.RESULT_CANCELED,
null,
)
assertThat(widget.isChecked).isTrue()
verify(mockSupervisionManager, never()).setSupervisionEnabled(true)
}
private fun setSupervisionEnabled(enabled: Boolean) = private fun setSupervisionEnabled(enabled: Boolean) =
mockSupervisionManager.stub { on { isSupervisionEnabled } doReturn enabled } mockSupervisionManager.stub { on { isSupervisionEnabled } doReturn enabled }
private fun getMainSwitchPreference(): MainSwitchPreference {
val widget: MainSwitchPreference = preference.createAndBindWidget(context)
mockLifeCycleContext.stub {
on { findPreference<Preference>(SupervisionMainSwitchPreference.KEY) } doReturn widget
on {
requirePreference<MainSwitchPreference>(SupervisionMainSwitchPreference.KEY)
} doReturn widget
}
return widget
}
private fun verifyConfirmSupervisionCredentialsActivityStarted() {
val intentCaptor = argumentCaptor<Intent>()
verify(mockLifeCycleContext)
.startActivityForResult(
intentCaptor.capture(),
eq(REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS),
eq(null),
)
assertThat(intentCaptor.allValues.size).isEqualTo(1)
assertThat(intentCaptor.firstValue.component?.className)
.isEqualTo(ConfirmSupervisionCredentialsActivity::class.java.name)
}
} }