Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9dd6d474 authored by Clara Thomas's avatar Clara Thomas Committed by Android (Google) Code Review
Browse files

Merge "Adds supervision enablement flow." into main

parents 8cd638ee 385698e3
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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"
+46 −16
Original line number Diff line number Diff line
@@ -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
@@ -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))
@@ -106,7 +123,7 @@ open class ConfirmSupervisionCredentialsActivity : FragmentActivity() {
            CancellationSignal(),
            ContextCompat.getMainExecutor(this),
            mAuthenticationCallback,
            supervisingUserId,
            userId
        )
    }

@@ -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.")
        }
    }
}
+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
    }

}
+21 −0
Original line number Diff line number Diff line
@@ -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
@@ -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) :
@@ -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)
@@ -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,
@@ -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
    }

}
+37 −8
Original line number Diff line number Diff line
@@ -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
@@ -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

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

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

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

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