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

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

Merge "Introduces an authentication session for the supervision PIN." into main

parents 5a595fd2 82151250
Loading
Loading
Loading
Loading
+19 −7
Original line number Diff line number Diff line
@@ -61,7 +61,8 @@ import com.android.settingslib.supervision.SupervisionLog.TAG
 */
class ConfirmSupervisionCredentialsActivity : FragmentActivity() {

    private val mAuthenticationCallback =
    @VisibleForTesting
    val mAuthenticationCallback =
        object : AuthenticationCallback() {
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                Log.w(TAG, "onAuthenticationError(errorCode=$errorCode, errString=$errString)")
@@ -70,6 +71,11 @@ class ConfirmSupervisionCredentialsActivity : FragmentActivity() {
            }

            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
                val authController =
                    SupervisionAuthController.getInstance(
                        this@ConfirmSupervisionCredentialsActivity
                    )
                authController.startSession(taskId)
                setResult(RESULT_OK)
                finish()
            }
@@ -117,13 +123,19 @@ class ConfirmSupervisionCredentialsActivity : FragmentActivity() {
            return
        }

        val authController = SupervisionAuthController.getInstance(this)
        if (!authController.isSessionActive(taskId)) {
            val activityManager = getSystemService(ActivityManager::class.java)
            if (!activityManager.startProfile(supervisingUser)) {
                errorHandler("Unable to start supervising user, cannot verify credentials.")
                return
            }

            showBiometricPrompt(supervisingUser.identifier)
        } else {
            Log.i(TAG, "Bypassing authentication due to active session")
            setResult(RESULT_OK)
            finish()
        }
    }

    @RequiresPermission(allOf = [USE_BIOMETRIC_INTERNAL, SET_BIOMETRIC_DIALOG_ADVANCED])
+102 −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.app.ActivityManager
import android.app.ActivityTaskManager
import android.app.TaskStackListener
import android.content.Context
import androidx.annotation.VisibleForTesting
import javax.annotation.concurrent.GuardedBy

/**
 * Manages a supervision authentication session.
 *
 * After the supervising profile is authenticated, the user should not need to authenticate again
 * unless they leave the Settings app. This session should persist across interactions with native
 * settings and those injected from the separate supervision APK.
 *
 * Activities responsible for authentication should start a session only after the user has been
 * successfully authenticated, and the session will be automatically invalidated when the task that
 * started the session stops running or goes into the background.
 */
class SupervisionAuthController private constructor(appContext: Context) {
    private val activityManager = appContext.getSystemService(ActivityManager::class.java)
    @GuardedBy("this") private var currentTaskId: Int? = null

    @VisibleForTesting
    val mTaskStackListener: TaskStackListener =
        object : TaskStackListener() {
            override fun onTaskStackChanged() {
                synchronized(this) {
                    if (currentTaskId != null && !isCurrentTaskFocused()) {
                        invalidateSession()
                    }
                }
            }
        }

    init {
        ActivityTaskManager.getInstance().registerTaskStackListener(mTaskStackListener)
    }

    /**
     * Starts an auth session, indicating that a parent has been authenticated on the current task.
     */
    fun startSession(taskId: Int) {
        synchronized(this) { currentTaskId = taskId }
    }

    /** Returns whether an auth session is currently active on this task. */
    fun isSessionActive(taskId: Int): Boolean {
        synchronized(this) {
            return currentTaskId == taskId
        }
    }

    /**
     * Invalidates the current session. This should only be done if a task with an active session
     * goes into the background.
     */
    @GuardedBy("this")
    private fun invalidateSession() {
        currentTaskId = null
    }

    /** Whether the task with a currently active auth session is running and focused. */
    @GuardedBy("this")
    private fun isCurrentTaskFocused(): Boolean {
        if (currentTaskId == null) return false
        val appTasks = activityManager.appTasks ?: emptyList()
        val task = appTasks.find { it.taskInfo.taskId == currentTaskId }
        if (task == null) return false
        return task.taskInfo.isRunning && task.taskInfo.isFocused
    }

    companion object {
        @Volatile @VisibleForTesting var sInstance: SupervisionAuthController? = null

        fun getInstance(context: Context): SupervisionAuthController {
            return sInstance
                ?: synchronized(this) {
                    sInstance
                        ?: SupervisionAuthController(context.applicationContext).also {
                            sInstance = it
                        }
                }
        }
    }
}
+26 −10
Original line number Diff line number Diff line
@@ -31,7 +31,6 @@ 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 androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
@@ -93,6 +92,7 @@ class ConfirmSupervisionCredentialsActivityTest {
            setSystemService(Context.SUPERVISION_SERVICE, mockSupervisionManager)
            setSystemService(Context.USER_SERVICE, mockUserManager)
        }
        SupervisionAuthController.sInstance = null
    }

    @Test
@@ -139,6 +139,20 @@ class ConfirmSupervisionCredentialsActivityTest {
        assertThat(shadowActivity.resultCode).isEqualTo(Activity.RESULT_CANCELED)
    }

    @Test
    fun onCreate_authSessionActive_finishWithResultOK() {
        ShadowRoleManager.addRoleHolder(ROLE_SYSTEM_SUPERVISION, callingPackage, currentUser)
        mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) }
        mockActivityManager.stub { on { startProfile(any()) } doReturn true }
        shadowKeyguardManager.setIsDeviceSecure(SUPERVISING_USER_ID, true)
        SupervisionAuthController.getInstance(context).startSession(mActivity.taskId)

        mActivityController.setup()

        assertThat(mActivity.isFinishing).isTrue()
        assertThat(shadowActivity.resultCode).isEqualTo(Activity.RESULT_OK)
    }

    @Test
    @Config(sdk = [Build.VERSION_CODES.BAKLAVA])
    fun onCreate_callerIsSystemUid_doesNotFinish() {
@@ -211,6 +225,17 @@ class ConfirmSupervisionCredentialsActivityTest {
        assertThat(biometricPrompt.contentView).isNull()
    }

    fun onAuthenticationSucceeded_startsAuthSession_returnsResultOK() {
        mockUserManager.stub { on { users } doReturn listOf(SUPERVISING_USER_INFO) }
        shadowKeyguardManager.setIsDeviceSecure(SUPERVISING_USER_ID, true)

        mActivity.mAuthenticationCallback.onAuthenticationSucceeded(null)

        assertThat(SupervisionAuthController.getInstance(context).isSessionActive(mActivity.taskId))
            .isTrue()
        assertThat(shadowActivity.resultCode).isEqualTo(Activity.RESULT_OK)
    }

    private companion object {
        const val SUPERVISING_USER_ID = 5
        val SUPERVISING_USER_INFO =
@@ -221,14 +246,5 @@ class ConfirmSupervisionCredentialsActivityTest {
                /* flags */ 0,
                USER_TYPE_PROFILE_SUPERVISING,
            )
        const val TESTING_USER_ID = 6
        val TESTING_USER_INFO =
            UserInfo(
                TESTING_USER_ID,
                /* name */ "testing",
                /* iconPath */ "",
                /* flags */ 0,
                USER_TYPE_PROFILE_TEST,
            )
    }
}
+99 −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.app.ActivityManager
import android.content.ContextWrapper
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub

@RunWith(AndroidJUnit4::class)
class SupervisionAuthControllerTest {

    private val mockActivityManager = mock<ActivityManager>()
    private val context =
        object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
            override fun getSystemService(name: String): Any =
                when (name) {
                    getSystemServiceName(ActivityManager::class.java) -> mockActivityManager
                    else -> super.getSystemService(name)
                }
        }

    @Before
    fun setUp() {
        SupervisionAuthController.sInstance = null
    }

    @Test
    fun initially_sessionIsNotActive() {
        val mockTask =
            mock<ActivityManager.AppTask>().stub { on { taskInfo } doReturn FOCUSED_TASK_INFO }
        mockActivityManager.stub { on { appTasks } doReturn listOf(mockTask) }

        val authController = SupervisionAuthController.getInstance(context)
        assertThat(authController.isSessionActive(TASK_ID)).isFalse()
    }

    @Test
    fun startSession_sessionIsActive() {
        val mockTask =
            mock<ActivityManager.AppTask>().stub { on { taskInfo } doReturn FOCUSED_TASK_INFO }
        mockActivityManager.stub { on { appTasks } doReturn listOf(mockTask) }

        val authController = SupervisionAuthController.getInstance(context)
        authController.startSession(TASK_ID)
        assertThat(authController.isSessionActive(TASK_ID)).isTrue()
    }

    @Test
    fun taskLosesFocus_sessionInvalidated() {
        val mockTask =
            mock<ActivityManager.AppTask>().stub { on { taskInfo } doReturn FOCUSED_TASK_INFO }
        mockActivityManager.stub { on { appTasks } doReturn listOf(mockTask) }

        val authController = SupervisionAuthController.getInstance(context)
        authController.startSession(TASK_ID)
        assertThat(authController.isSessionActive(TASK_ID)).isTrue()

        mockTask.stub { on { taskInfo } doReturn NOT_FOCUSED_TASK_INFO }
        authController.mTaskStackListener.onTaskStackChanged()
        assertThat(authController.isSessionActive(TASK_ID)).isFalse()
    }

    private companion object {
        const val TASK_ID = 100
        val FOCUSED_TASK_INFO =
            ActivityManager.RecentTaskInfo().apply {
                taskId = TASK_ID
                isRunning = true
                isFocused = true
            }
        val NOT_FOCUSED_TASK_INFO =
            ActivityManager.RecentTaskInfo().apply {
                taskId = TASK_ID
                isRunning = true
                isFocused = false
            }
    }
}