Loading AndroidManifest.xml +2 −0 Original line number Diff line number Diff line Loading @@ -2870,6 +2870,8 @@ </intent-filter> </activity> <activity android:name=".supervision.SetupSupervisionActivity" android:exported="false" /> <activity android:name=".SetupRedactionInterstitial" android:enabled="false" android:exported="true" Loading src/com/android/settings/supervision/ConfirmSupervisionCredentialsActivity.kt +46 −16 Original line number Diff line number Diff line Loading @@ -15,8 +15,10 @@ */ package com.android.settings.supervision import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL import android.Manifest.permission.MANAGE_USERS import android.Manifest.permission.USE_BIOMETRIC_INTERNAL import android.app.Activity import android.app.ActivityManager import android.app.role.RoleManager import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.BiometricPrompt Loading Loading @@ -52,50 +54,65 @@ import com.android.settingslib.supervision.SupervisionLog */ @OpenForTesting open class ConfirmSupervisionCredentialsActivity : FragmentActivity() { private val mAuthenticationCallback = object : AuthenticationCallback() { @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { tryStopProfile() Log.w( SupervisionLog.TAG, "onAuthenticationError(errorCode=$errorCode, errString=$errString)", ) setResult(Activity.RESULT_CANCELED) setResult(RESULT_CANCELED) finish() } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) { setResult(Activity.RESULT_OK) tryStopProfile() setResult(RESULT_OK) finish() } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) override fun onAuthenticationFailed() { setResult(Activity.RESULT_CANCELED) tryStopProfile() setResult(RESULT_CANCELED) finish() } } @RequiresPermission(USE_BIOMETRIC_INTERNAL) @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!callerHasSupervisionRole() && !callerIsSystemUid()) { setResult(Activity.RESULT_CANCELED) setResult(RESULT_CANCELED) finish() return } showBiometricPrompt() val supervisingUser = SupervisionHelper.getInstance(this).getSupervisingUserHandle() if (supervisingUser == null) { Log.w(SupervisionLog.TAG, "No supervising user exists, cannot verify credentials.") setResult(RESULT_CANCELED) finish() return } @RequiresPermission(USE_BIOMETRIC_INTERNAL) fun showBiometricPrompt() { val supervisingUserId = SupervisionHelper.getInstance(this).getSupervisingUserHandle()?.identifier if (supervisingUserId == null) { Log.w(SupervisionLog.TAG, "supervisingUserId is null") setResult(Activity.RESULT_CANCELED) val activityManager = getSystemService(ActivityManager::class.java) if(!activityManager.startProfile(supervisingUser)) { Log.w(SupervisionLog.TAG, "Unable to start supervising user, cannot verify credentials.") setResult(RESULT_CANCELED) finish() return } showBiometricPrompt(supervisingUser.identifier) } @RequiresPermission(USE_BIOMETRIC_INTERNAL) fun showBiometricPrompt(userId: Int) { val biometricPrompt = BiometricPrompt.Builder(this) .setTitle(getString(R.string.supervision_full_screen_pin_verification_title)) Loading @@ -106,7 +123,7 @@ open class ConfirmSupervisionCredentialsActivity : FragmentActivity() { CancellationSignal(), ContextCompat.getMainExecutor(this), mAuthenticationCallback, supervisingUserId, userId ) } Loading @@ -129,4 +146,17 @@ open class ConfirmSupervisionCredentialsActivity : FragmentActivity() { } return true } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) private fun tryStopProfile() { val supervisingUser = SupervisionHelper.getInstance(this).getSupervisingUserHandle() val activityManager = getSystemService(ActivityManager::class.java) if (supervisingUser == null) { Log.w(SupervisionLog.TAG, "Cannot stop supervising profile because it does not exist.") return } if (!activityManager.stopProfile(supervisingUser)) { Log.w(SupervisionLog.TAG, "Could not stop the supervising profile.") } } } src/com/android/settings/supervision/SetupSupervisionActivity.kt 0 → 100644 +139 −0 Original line number Diff line number Diff line /* * 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.CREATE_USERS import android.Manifest.permission.INTERACT_ACROSS_USERS import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL import android.Manifest.permission.MANAGE_USERS import android.app.ActivityManager import android.app.KeyguardManager import android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD import android.app.admin.DevicePolicyManager.EXTRA_PASSWORD_COMPLEXITY import android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW import android.content.Intent import android.os.Bundle import android.os.UserHandle import android.os.UserManager import android.os.UserManager.USER_TYPE_PROFILE_SUPERVISING import android.util.Log import androidx.annotation.RequiresPermission import androidx.fragment.app.FragmentActivity import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_FINGERPRINT_ENROLLMENT_ONLY import com.android.settingslib.supervision.SupervisionLog /** * This activity starts the flow for setting up device supervision. * * Three things are required for device supervision: a supervising profile must be created, a lock * must be set up for this profile, and `SupervisionManager.isSupervisionEnabled()` must be set. * This activity handles the first two, while the third is managed by * `SupervisionMainSwitchPreference`. * * Returns `Activity.RESULT_OK` if all steps of setup succeed, and `Activity.RESULT_CANCELED` if * any step fails, or the user does not finish setting up the lock for the supervising profile. * * Usage: * 1. Start this activity using `startActivityForResult()`. * 2. Handle the result in `onActivityResult()`. */ class SetupSupervisionActivity : FragmentActivity() { public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) { enableSupervision() } } @RequiresPermission(anyOf = [CREATE_USERS, MANAGE_USERS]) private fun enableSupervision() { val supervisionHelper = SupervisionHelper.getInstance(this) var supervisingUser = supervisionHelper.getSupervisingUserHandle() // If a supervising profile does not already exist on the device, create one if (supervisingUser == null) { val userManager = getSystemService(UserManager::class.java) val userInfo = userManager.createUser("Supervising", USER_TYPE_PROFILE_SUPERVISING, /* flags= */ 0) if (userInfo != null) { supervisingUser = userInfo.userHandle } else { // TODO(399705794): Surface this error to user Log.w(SupervisionLog.TAG, "Unable to create supervising profile.") setResult(RESULT_CANCELED) finish() return } } val activityManager = getSystemService(ActivityManager::class.java) if (!activityManager.startProfile(supervisingUser)) { // TODO(399705794): Surface this error to user Log.w(SupervisionLog.TAG, "Could not start supervising profile.") setResult(RESULT_CANCELED) finish() return } startChooseLockActivity(supervisingUser) } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, INTERACT_ACROSS_USERS]) private fun startChooseLockActivity(userHandle: UserHandle) { // TODO(b/389712273) Intent directly to PIN selection screen to avoid giving the user // the options for password/pattern during the initial setup flow val intent = Intent(ACTION_SET_NEW_PASSWORD).apply { putExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_LOW) putExtra(EXTRA_KEY_FINGERPRINT_ENROLLMENT_ONLY, true) } startActivityForResultAsUser(intent, REQUEST_CODE_SET_SUPERVISION_LOCK, userHandle) } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) val supervisionHelper = SupervisionHelper.getInstance(this) val supervisingUser = supervisionHelper.getSupervisingUserHandle() if (supervisingUser == null) { Log.w(SupervisionLog.TAG, "No supervising user handle found after lock setup.") setResult(RESULT_CANCELED) finish() return } val activityManager = getSystemService(ActivityManager::class.java) tryStopProfile(supervisingUser, activityManager) val keyguardManager = getSystemService(KeyguardManager::class.java) if (!keyguardManager.isDeviceSecure(supervisingUser.identifier)) { Log.w(SupervisionLog.TAG, "Lock for supervising user not set up.") setResult(RESULT_CANCELED) finish() return } setResult(RESULT_OK) finish() } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) private fun tryStopProfile(supervisingUser: UserHandle, activityManager: ActivityManager) { if (!activityManager.stopProfile(supervisingUser)) { Log.w(SupervisionLog.TAG, "Could not stop the supervising profile.") } } companion object { private const val REQUEST_CODE_SET_SUPERVISION_LOCK = 0 } } src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt +21 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,8 @@ import android.app.Activity import android.app.supervision.SupervisionManager import android.content.Context import android.content.Intent import android.util.Log import androidx.annotation.VisibleForTesting import androidx.preference.Preference import com.android.settings.R import com.android.settingslib.datastore.KeyValueStore Loading @@ -32,6 +34,7 @@ import com.android.settingslib.metadata.ReadWritePermit import com.android.settingslib.metadata.SensitivityLevel import com.android.settingslib.preference.MainSwitchPreferenceBinding import com.android.settingslib.preference.forEachRecursively import com.android.settingslib.supervision.SupervisionLog /** Main toggle to enable or disable device supervision. */ class SupervisionMainSwitchPreference(context: Context) : Loading Loading @@ -77,6 +80,10 @@ class SupervisionMainSwitchPreference(context: Context) : resultCode: Int, data: Intent?, ): Boolean { if (requestCode != REQUEST_CODE_SET_UP_SUPERVISION && requestCode != REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS) { return false } if (resultCode == Activity.RESULT_OK) { val mainSwitchPreference = lifeCycleContext.requirePreference<com.android.settingslib.widget.MainSwitchPreference>(KEY) Loading @@ -96,7 +103,17 @@ class SupervisionMainSwitchPreference(context: Context) : override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { if (newValue !is Boolean) return true val supervisionHelper = SupervisionHelper.getInstance(preference.context) // If supervision is being enabled but either the supervising profile hasn't been created // or the credentials aren't set, launch SetupSupervisionActivity. if (newValue && !supervisionHelper.isSupervisingCredentialSet()) { val intent = Intent(lifeCycleContext, SetupSupervisionActivity::class.java) lifeCycleContext.startActivityForResult(intent, REQUEST_CODE_SET_UP_SUPERVISION, null) return false } // If supervision is already set up, confirm credentials before any change. val intent = Intent(lifeCycleContext, ConfirmSupervisionCredentialsActivity::class.java) lifeCycleContext.startActivityForResult( intent, Loading Loading @@ -137,6 +154,10 @@ class SupervisionMainSwitchPreference(context: Context) : companion object { const val KEY = "device_supervision_switch" @VisibleForTesting const val REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS = 0 @VisibleForTesting const val REQUEST_CODE_SET_UP_SUPERVISION = 1 } } tests/robotests/src/com/android/settings/supervision/ConfirmSupervisionCredentialsActivityTest.kt +37 −8 Original line number Diff line number Diff line Loading @@ -16,23 +16,26 @@ package com.android.settings.supervision import android.app.Activity import android.app.ActivityManager import android.app.role.RoleManager import android.content.pm.UserInfo import android.os.Build import android.os.Process import android.os.UserHandle import android.os.UserManager import android.os.UserManager.USER_TYPE_PROFILE_SUPERVISING import android.os.UserManager.USER_TYPE_PROFILE_TEST import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.stub import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config Loading @@ -42,6 +45,7 @@ import org.robolectric.shadows.ShadowBinder class ConfirmSupervisionCredentialsActivityTest { private val mockRoleManager = mock<RoleManager>() private val mockUserManager = mock<UserManager>() private val mockActivityManager = mock<ActivityManager>() private lateinit var mActivity: ConfirmSupervisionCredentialsActivity Loading @@ -56,24 +60,45 @@ class ConfirmSupervisionCredentialsActivityTest { ) { on { getSystemService(RoleManager::class.java) } doReturn mockRoleManager on { getSystemService(UserManager::class.java) } doReturn mockUserManager on { getSystemService(ActivityManager::class.java) } doReturn mockActivityManager on { callingPackage } doReturn callingPackage } } @Test fun onCreate_callerHasSupervisionRole_doesNotFinish() { whenever(mockRoleManager.getRoleHolders(any())).thenReturn(listOf(callingPackage)) whenever(mockUserManager.users).thenReturn(listOf(SUPERVISING_USER_INFO)) mockRoleManager.stub { on { getRoleHolders(any()) } doReturn listOf(callingPackage) } mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn true } mActivity.onCreate(null) verify(mActivity, never()).finish() // Ensure that the supervising profile is started val userCaptor = argumentCaptor<UserHandle>() verify(mockActivityManager).startProfile(userCaptor.capture()) assert(userCaptor.lastValue.identifier == SUPERVISING_USER_ID) } @Test fun onCreate_failsToStartSupervisingProfile_finish() { mockRoleManager.stub { on { getRoleHolders(any()) } doReturn listOf(callingPackage) } mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn false } mActivity.onCreate(null) verify(mActivity).setResult(Activity.RESULT_CANCELED) verify(mActivity).finish() } @Test fun onCreate_callerNotHasSupervisionRole_finish() { val otherPackage = "com.example.other" whenever(mockRoleManager.getRoleHolders(any())).thenReturn(listOf(otherPackage)) mockRoleManager.stub { on { getRoleHolders(any()) } doReturn listOf(otherPackage) } mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn true } mActivity.onCreate(null) Loading @@ -85,7 +110,8 @@ class ConfirmSupervisionCredentialsActivityTest { @Config(sdk = [Build.VERSION_CODES.BAKLAVA]) fun onCreate_callerIsSystemUid_doesNotFinish() { ShadowBinder.setCallingUid(Process.SYSTEM_UID) whenever(mockUserManager.users).thenReturn(listOf(SUPERVISING_USER_INFO)) mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn true } mActivity.onCreate(null) Loading @@ -96,6 +122,8 @@ class ConfirmSupervisionCredentialsActivityTest { @Config(sdk = [Build.VERSION_CODES.BAKLAVA]) fun onCreate_callerIsUnknownUid_finish() { ShadowBinder.setCallingUid(Process.NOBODY_UID) mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn true } mActivity.onCreate(null) Loading @@ -105,8 +133,9 @@ class ConfirmSupervisionCredentialsActivityTest { @Test fun onCreate_noSupervisingCredential_finish() { whenever(mockRoleManager.getRoleHolders(any())).thenReturn(listOf(callingPackage)) whenever(mockUserManager.users).thenReturn(listOf(TESTING_USER_INFO)) mockRoleManager.stub { on { getRoleHolders(any()) } doReturn listOf(callingPackage) } mockUserManager.stub { on { users } doReturn listOf(TESTING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn true } mActivity.onCreate(null) Loading Loading
AndroidManifest.xml +2 −0 Original line number Diff line number Diff line Loading @@ -2870,6 +2870,8 @@ </intent-filter> </activity> <activity android:name=".supervision.SetupSupervisionActivity" android:exported="false" /> <activity android:name=".SetupRedactionInterstitial" android:enabled="false" android:exported="true" Loading
src/com/android/settings/supervision/ConfirmSupervisionCredentialsActivity.kt +46 −16 Original line number Diff line number Diff line Loading @@ -15,8 +15,10 @@ */ package com.android.settings.supervision import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL import android.Manifest.permission.MANAGE_USERS import android.Manifest.permission.USE_BIOMETRIC_INTERNAL import android.app.Activity import android.app.ActivityManager import android.app.role.RoleManager import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.BiometricPrompt Loading Loading @@ -52,50 +54,65 @@ import com.android.settingslib.supervision.SupervisionLog */ @OpenForTesting open class ConfirmSupervisionCredentialsActivity : FragmentActivity() { private val mAuthenticationCallback = object : AuthenticationCallback() { @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { tryStopProfile() Log.w( SupervisionLog.TAG, "onAuthenticationError(errorCode=$errorCode, errString=$errString)", ) setResult(Activity.RESULT_CANCELED) setResult(RESULT_CANCELED) finish() } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) { setResult(Activity.RESULT_OK) tryStopProfile() setResult(RESULT_OK) finish() } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) override fun onAuthenticationFailed() { setResult(Activity.RESULT_CANCELED) tryStopProfile() setResult(RESULT_CANCELED) finish() } } @RequiresPermission(USE_BIOMETRIC_INTERNAL) @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!callerHasSupervisionRole() && !callerIsSystemUid()) { setResult(Activity.RESULT_CANCELED) setResult(RESULT_CANCELED) finish() return } showBiometricPrompt() val supervisingUser = SupervisionHelper.getInstance(this).getSupervisingUserHandle() if (supervisingUser == null) { Log.w(SupervisionLog.TAG, "No supervising user exists, cannot verify credentials.") setResult(RESULT_CANCELED) finish() return } @RequiresPermission(USE_BIOMETRIC_INTERNAL) fun showBiometricPrompt() { val supervisingUserId = SupervisionHelper.getInstance(this).getSupervisingUserHandle()?.identifier if (supervisingUserId == null) { Log.w(SupervisionLog.TAG, "supervisingUserId is null") setResult(Activity.RESULT_CANCELED) val activityManager = getSystemService(ActivityManager::class.java) if(!activityManager.startProfile(supervisingUser)) { Log.w(SupervisionLog.TAG, "Unable to start supervising user, cannot verify credentials.") setResult(RESULT_CANCELED) finish() return } showBiometricPrompt(supervisingUser.identifier) } @RequiresPermission(USE_BIOMETRIC_INTERNAL) fun showBiometricPrompt(userId: Int) { val biometricPrompt = BiometricPrompt.Builder(this) .setTitle(getString(R.string.supervision_full_screen_pin_verification_title)) Loading @@ -106,7 +123,7 @@ open class ConfirmSupervisionCredentialsActivity : FragmentActivity() { CancellationSignal(), ContextCompat.getMainExecutor(this), mAuthenticationCallback, supervisingUserId, userId ) } Loading @@ -129,4 +146,17 @@ open class ConfirmSupervisionCredentialsActivity : FragmentActivity() { } return true } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) private fun tryStopProfile() { val supervisingUser = SupervisionHelper.getInstance(this).getSupervisingUserHandle() val activityManager = getSystemService(ActivityManager::class.java) if (supervisingUser == null) { Log.w(SupervisionLog.TAG, "Cannot stop supervising profile because it does not exist.") return } if (!activityManager.stopProfile(supervisingUser)) { Log.w(SupervisionLog.TAG, "Could not stop the supervising profile.") } } }
src/com/android/settings/supervision/SetupSupervisionActivity.kt 0 → 100644 +139 −0 Original line number Diff line number Diff line /* * 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.CREATE_USERS import android.Manifest.permission.INTERACT_ACROSS_USERS import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL import android.Manifest.permission.MANAGE_USERS import android.app.ActivityManager import android.app.KeyguardManager import android.app.admin.DevicePolicyManager.ACTION_SET_NEW_PASSWORD import android.app.admin.DevicePolicyManager.EXTRA_PASSWORD_COMPLEXITY import android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW import android.content.Intent import android.os.Bundle import android.os.UserHandle import android.os.UserManager import android.os.UserManager.USER_TYPE_PROFILE_SUPERVISING import android.util.Log import androidx.annotation.RequiresPermission import androidx.fragment.app.FragmentActivity import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_FINGERPRINT_ENROLLMENT_ONLY import com.android.settingslib.supervision.SupervisionLog /** * This activity starts the flow for setting up device supervision. * * Three things are required for device supervision: a supervising profile must be created, a lock * must be set up for this profile, and `SupervisionManager.isSupervisionEnabled()` must be set. * This activity handles the first two, while the third is managed by * `SupervisionMainSwitchPreference`. * * Returns `Activity.RESULT_OK` if all steps of setup succeed, and `Activity.RESULT_CANCELED` if * any step fails, or the user does not finish setting up the lock for the supervising profile. * * Usage: * 1. Start this activity using `startActivityForResult()`. * 2. Handle the result in `onActivityResult()`. */ class SetupSupervisionActivity : FragmentActivity() { public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) { enableSupervision() } } @RequiresPermission(anyOf = [CREATE_USERS, MANAGE_USERS]) private fun enableSupervision() { val supervisionHelper = SupervisionHelper.getInstance(this) var supervisingUser = supervisionHelper.getSupervisingUserHandle() // If a supervising profile does not already exist on the device, create one if (supervisingUser == null) { val userManager = getSystemService(UserManager::class.java) val userInfo = userManager.createUser("Supervising", USER_TYPE_PROFILE_SUPERVISING, /* flags= */ 0) if (userInfo != null) { supervisingUser = userInfo.userHandle } else { // TODO(399705794): Surface this error to user Log.w(SupervisionLog.TAG, "Unable to create supervising profile.") setResult(RESULT_CANCELED) finish() return } } val activityManager = getSystemService(ActivityManager::class.java) if (!activityManager.startProfile(supervisingUser)) { // TODO(399705794): Surface this error to user Log.w(SupervisionLog.TAG, "Could not start supervising profile.") setResult(RESULT_CANCELED) finish() return } startChooseLockActivity(supervisingUser) } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, INTERACT_ACROSS_USERS]) private fun startChooseLockActivity(userHandle: UserHandle) { // TODO(b/389712273) Intent directly to PIN selection screen to avoid giving the user // the options for password/pattern during the initial setup flow val intent = Intent(ACTION_SET_NEW_PASSWORD).apply { putExtra(EXTRA_PASSWORD_COMPLEXITY, PASSWORD_COMPLEXITY_LOW) putExtra(EXTRA_KEY_FINGERPRINT_ENROLLMENT_ONLY, true) } startActivityForResultAsUser(intent, REQUEST_CODE_SET_SUPERVISION_LOCK, userHandle) } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) val supervisionHelper = SupervisionHelper.getInstance(this) val supervisingUser = supervisionHelper.getSupervisingUserHandle() if (supervisingUser == null) { Log.w(SupervisionLog.TAG, "No supervising user handle found after lock setup.") setResult(RESULT_CANCELED) finish() return } val activityManager = getSystemService(ActivityManager::class.java) tryStopProfile(supervisingUser, activityManager) val keyguardManager = getSystemService(KeyguardManager::class.java) if (!keyguardManager.isDeviceSecure(supervisingUser.identifier)) { Log.w(SupervisionLog.TAG, "Lock for supervising user not set up.") setResult(RESULT_CANCELED) finish() return } setResult(RESULT_OK) finish() } @RequiresPermission(anyOf = [INTERACT_ACROSS_USERS_FULL, MANAGE_USERS]) private fun tryStopProfile(supervisingUser: UserHandle, activityManager: ActivityManager) { if (!activityManager.stopProfile(supervisingUser)) { Log.w(SupervisionLog.TAG, "Could not stop the supervising profile.") } } companion object { private const val REQUEST_CODE_SET_SUPERVISION_LOCK = 0 } }
src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt +21 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,8 @@ import android.app.Activity import android.app.supervision.SupervisionManager import android.content.Context import android.content.Intent import android.util.Log import androidx.annotation.VisibleForTesting import androidx.preference.Preference import com.android.settings.R import com.android.settingslib.datastore.KeyValueStore Loading @@ -32,6 +34,7 @@ import com.android.settingslib.metadata.ReadWritePermit import com.android.settingslib.metadata.SensitivityLevel import com.android.settingslib.preference.MainSwitchPreferenceBinding import com.android.settingslib.preference.forEachRecursively import com.android.settingslib.supervision.SupervisionLog /** Main toggle to enable or disable device supervision. */ class SupervisionMainSwitchPreference(context: Context) : Loading Loading @@ -77,6 +80,10 @@ class SupervisionMainSwitchPreference(context: Context) : resultCode: Int, data: Intent?, ): Boolean { if (requestCode != REQUEST_CODE_SET_UP_SUPERVISION && requestCode != REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS) { return false } if (resultCode == Activity.RESULT_OK) { val mainSwitchPreference = lifeCycleContext.requirePreference<com.android.settingslib.widget.MainSwitchPreference>(KEY) Loading @@ -96,7 +103,17 @@ class SupervisionMainSwitchPreference(context: Context) : override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { if (newValue !is Boolean) return true val supervisionHelper = SupervisionHelper.getInstance(preference.context) // If supervision is being enabled but either the supervising profile hasn't been created // or the credentials aren't set, launch SetupSupervisionActivity. if (newValue && !supervisionHelper.isSupervisingCredentialSet()) { val intent = Intent(lifeCycleContext, SetupSupervisionActivity::class.java) lifeCycleContext.startActivityForResult(intent, REQUEST_CODE_SET_UP_SUPERVISION, null) return false } // If supervision is already set up, confirm credentials before any change. val intent = Intent(lifeCycleContext, ConfirmSupervisionCredentialsActivity::class.java) lifeCycleContext.startActivityForResult( intent, Loading Loading @@ -137,6 +154,10 @@ class SupervisionMainSwitchPreference(context: Context) : companion object { const val KEY = "device_supervision_switch" @VisibleForTesting const val REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS = 0 @VisibleForTesting const val REQUEST_CODE_SET_UP_SUPERVISION = 1 } }
tests/robotests/src/com/android/settings/supervision/ConfirmSupervisionCredentialsActivityTest.kt +37 −8 Original line number Diff line number Diff line Loading @@ -16,23 +16,26 @@ package com.android.settings.supervision import android.app.Activity import android.app.ActivityManager import android.app.role.RoleManager import android.content.pm.UserInfo import android.os.Build import android.os.Process import android.os.UserHandle import android.os.UserManager import android.os.UserManager.USER_TYPE_PROFILE_SUPERVISING import android.os.UserManager.USER_TYPE_PROFILE_TEST import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.stub import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config Loading @@ -42,6 +45,7 @@ import org.robolectric.shadows.ShadowBinder class ConfirmSupervisionCredentialsActivityTest { private val mockRoleManager = mock<RoleManager>() private val mockUserManager = mock<UserManager>() private val mockActivityManager = mock<ActivityManager>() private lateinit var mActivity: ConfirmSupervisionCredentialsActivity Loading @@ -56,24 +60,45 @@ class ConfirmSupervisionCredentialsActivityTest { ) { on { getSystemService(RoleManager::class.java) } doReturn mockRoleManager on { getSystemService(UserManager::class.java) } doReturn mockUserManager on { getSystemService(ActivityManager::class.java) } doReturn mockActivityManager on { callingPackage } doReturn callingPackage } } @Test fun onCreate_callerHasSupervisionRole_doesNotFinish() { whenever(mockRoleManager.getRoleHolders(any())).thenReturn(listOf(callingPackage)) whenever(mockUserManager.users).thenReturn(listOf(SUPERVISING_USER_INFO)) mockRoleManager.stub { on { getRoleHolders(any()) } doReturn listOf(callingPackage) } mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn true } mActivity.onCreate(null) verify(mActivity, never()).finish() // Ensure that the supervising profile is started val userCaptor = argumentCaptor<UserHandle>() verify(mockActivityManager).startProfile(userCaptor.capture()) assert(userCaptor.lastValue.identifier == SUPERVISING_USER_ID) } @Test fun onCreate_failsToStartSupervisingProfile_finish() { mockRoleManager.stub { on { getRoleHolders(any()) } doReturn listOf(callingPackage) } mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn false } mActivity.onCreate(null) verify(mActivity).setResult(Activity.RESULT_CANCELED) verify(mActivity).finish() } @Test fun onCreate_callerNotHasSupervisionRole_finish() { val otherPackage = "com.example.other" whenever(mockRoleManager.getRoleHolders(any())).thenReturn(listOf(otherPackage)) mockRoleManager.stub { on { getRoleHolders(any()) } doReturn listOf(otherPackage) } mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn true } mActivity.onCreate(null) Loading @@ -85,7 +110,8 @@ class ConfirmSupervisionCredentialsActivityTest { @Config(sdk = [Build.VERSION_CODES.BAKLAVA]) fun onCreate_callerIsSystemUid_doesNotFinish() { ShadowBinder.setCallingUid(Process.SYSTEM_UID) whenever(mockUserManager.users).thenReturn(listOf(SUPERVISING_USER_INFO)) mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn true } mActivity.onCreate(null) Loading @@ -96,6 +122,8 @@ class ConfirmSupervisionCredentialsActivityTest { @Config(sdk = [Build.VERSION_CODES.BAKLAVA]) fun onCreate_callerIsUnknownUid_finish() { ShadowBinder.setCallingUid(Process.NOBODY_UID) mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn true } mActivity.onCreate(null) Loading @@ -105,8 +133,9 @@ class ConfirmSupervisionCredentialsActivityTest { @Test fun onCreate_noSupervisingCredential_finish() { whenever(mockRoleManager.getRoleHolders(any())).thenReturn(listOf(callingPackage)) whenever(mockUserManager.users).thenReturn(listOf(TESTING_USER_INFO)) mockRoleManager.stub { on { getRoleHolders(any()) } doReturn listOf(callingPackage) } mockUserManager.stub { on { users } doReturn listOf(TESTING_USER_INFO) } mockActivityManager.stub { on { startProfile(any()) } doReturn true } mActivity.onCreate(null) Loading