[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
diff --git a/Android.bp b/Android.bp
index 150bdaf..c0a4c7c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -139,6 +139,7 @@
"aconfig_settings_flags",
"aconfig_settingslib_flags",
"android.app.flags-aconfig",
+ "android.app.supervision.flags-aconfig",
"android.provider.flags-aconfig",
"android.security.flags-aconfig",
"android.view.contentprotection.flags-aconfig",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8f5699c..90119a9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2817,6 +2817,15 @@
</intent-filter>
</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"
android:enabled="false"
android:exported="true"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 79020f9..104edfe 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -14309,5 +14309,7 @@
<!-- 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>
<!-- 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>
</resources>
diff --git a/src/com/android/settings/supervision/ConfirmSupervisionCredentialsActivity.kt b/src/com/android/settings/supervision/ConfirmSupervisionCredentialsActivity.kt
new file mode 100644
index 0000000..b459f53
--- /dev/null
+++ b/src/com/android/settings/supervision/ConfirmSupervisionCredentialsActivity.kt
@@ -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"
+ }
+}
diff --git a/src/com/android/settings/supervision/SupervisionDashboardScreen.kt b/src/com/android/settings/supervision/SupervisionDashboardScreen.kt
index 86f77f7..674c0f3a 100644
--- a/src/com/android/settings/supervision/SupervisionDashboardScreen.kt
+++ b/src/com/android/settings/supervision/SupervisionDashboardScreen.kt
@@ -56,7 +56,7 @@
override fun getPreferenceHierarchy(context: Context) =
preferenceHierarchy(context, this) {
- +SupervisionMainSwitchPreference()
+ +SupervisionMainSwitchPreference(context)
+TitlelessPreferenceGroup(SUPERVISION_DYNAMIC_GROUP_1) += {
+SupervisionWebContentFiltersScreen.KEY
}
diff --git a/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt b/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt
index 5a84137..88afc55 100644
--- a/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt
+++ b/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt
@@ -15,8 +15,10 @@
*/
package com.android.settings.supervision
+import android.app.Activity
import android.app.supervision.SupervisionManager
import android.content.Context
+import android.content.Intent
import androidx.preference.Preference
import com.android.settings.R
import com.android.settingslib.datastore.KeyValueStore
@@ -32,19 +34,22 @@
import com.android.settingslib.preference.forEachRecursively
/** Main toggle to enable or disable device supervision. */
-class SupervisionMainSwitchPreference :
+class SupervisionMainSwitchPreference(context: Context) :
MainSwitchPreference(KEY, R.string.device_supervision_switch_title),
PreferenceSummaryProvider,
MainSwitchPreferenceBinding,
Preference.OnPreferenceChangeListener,
PreferenceLifecycleProvider {
+ private val supervisionMainSwitchStorage = SupervisionMainSwitchStorage(context)
+ private lateinit var lifeCycleContext: PreferenceLifecycleContext
+
// 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 storage(context: Context): KeyValueStore = supervisionMainSwitchStorage
override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) =
ReadWritePermit.DISALLOW
@@ -55,26 +60,49 @@
override val sensitivityLevel: Int
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) {
super.bind(preference, metadata)
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 {
if (newValue !is Boolean) return true
- updateDependentPreferencesEnabledState(preference, newValue)
-
- return true
+ val intent = Intent(lifeCycleContext, ConfirmSupervisionCredentialsActivity::class.java)
+ lifeCycleContext.startActivityForResult(
+ intent,
+ REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS,
+ null,
+ )
+ return false
}
private fun updateDependentPreferencesEnabledState(
@@ -83,9 +111,8 @@
) {
preference?.parent?.forEachRecursively {
if (
- it.parent?.key?.toString() ==
- SupervisionDashboardScreen.SUPERVISION_DYNAMIC_GROUP_1 ||
- it.key?.toString() == SupervisionPinManagementScreen.KEY
+ it.parent?.key == SupervisionDashboardScreen.SUPERVISION_DYNAMIC_GROUP_1 ||
+ it.key == SupervisionPinManagementScreen.KEY
) {
it.isEnabled = isChecked
}
@@ -103,7 +130,6 @@
as 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) {
val supervisionManager = context.getSystemService(SupervisionManager::class.java)
supervisionManager?.setSupervisionEnabled(value)
@@ -113,5 +139,6 @@
companion object {
const val KEY = "device_supervision_switch"
+ const val REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS = 0
}
}
diff --git a/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt b/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt
index d5fa297..bf579d9 100644
--- a/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt
+++ b/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt
@@ -15,6 +15,7 @@
*/
package com.android.settings.supervision
+import android.app.Activity
import android.app.supervision.flags.Flags
import android.content.Context
import android.platform.test.annotations.DisableFlags
@@ -24,6 +25,7 @@
import androidx.preference.Preference
import androidx.test.core.app.ApplicationProvider
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.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -57,7 +59,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_SUPERVISION_SETTINGS_SCREEN)
- fun toggleMainSwitch_disablesChildPreferences() {
+ fun toggleMainSwitch_pinVerificationSucceeded_enablesChildPreferences() {
FragmentScenario.launchInContainer(preferenceScreenCreator.fragmentClass()).onFragment {
fragment ->
val mainSwitchPreference =
@@ -68,8 +70,38 @@
assertThat(childPreference.isEnabled).isFalse()
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()
}
}
+
+ @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()
+ }
+ }
}
diff --git a/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt b/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt
index 8b15c29..c7c393d 100644
--- a/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt
+++ b/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt
@@ -15,25 +15,33 @@
*/
package com.android.settings.supervision
+import android.app.Activity
import android.app.supervision.SupervisionManager
import android.content.Context
import android.content.ContextWrapper
+import android.content.Intent
+import androidx.preference.Preference
import androidx.test.core.app.ApplicationProvider
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.widget.MainSwitchPreference
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
class SupervisionMainSwitchPreferenceTest {
- private val preference = SupervisionMainSwitchPreference()
-
+ private val mockLifeCycleContext = mock<PreferenceLifecycleContext>()
private val mockSupervisionManager = mock<SupervisionManager>()
private val appContext: Context = ApplicationProvider.getApplicationContext()
@@ -46,6 +54,13 @@
}
}
+ private val preference = SupervisionMainSwitchPreference(context)
+
+ @Before
+ fun setUp() {
+ preference.onCreate(mockLifeCycleContext)
+ }
+
@Test
fun checked_supervisionEnabled_returnTrue() {
setSupervisionEnabled(true)
@@ -61,7 +76,7 @@
}
@Test
- fun toggleOn() {
+ fun toggleOn_triggersPinVerification() {
setSupervisionEnabled(false)
val widget = getMainSwitchPreference()
@@ -69,26 +84,90 @@
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()
verify(mockSupervisionManager).setSupervisionEnabled(true)
}
@Test
- fun toggleOff() {
+ fun toggleOff_pinVerificationSucceeded_supervisionDisabled() {
setSupervisionEnabled(true)
val widget = getMainSwitchPreference()
assertThat(widget.isChecked).isTrue()
- widget.performClick()
+ preference.onActivityResult(
+ mockLifeCycleContext,
+ REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS,
+ Activity.RESULT_OK,
+ null,
+ )
assertThat(widget.isChecked).isFalse()
verify(mockSupervisionManager).setSupervisionEnabled(false)
}
- private fun getMainSwitchPreference(): MainSwitchPreference =
- preference.createAndBindWidget(context)
+ @Test
+ 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) =
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)
+ }
}