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

Commit 1412f391 authored by Joshua Mccloskey's avatar Joshua Mccloskey Committed by Android (Google) Code Review
Browse files

Merge "Implement basic Fingerprint functionality." into udc-qpr-dev

parents 7fd37309 5a4211ec
Loading
Loading
Loading
Loading
+34 −1
Original line number Diff line number Diff line
@@ -15,4 +15,37 @@
  ~ limitations under the License.
  -->

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"/>
 No newline at end of file
<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:settings="http://schemas.android.com/apk/res-auto"
    android:title="@string/security_settings_fingerprint_preference_title">

    <PreferenceCategory
        android:key="security_settings_fingerprints_enrolled"
        settings:controller="com.android.settings.biometrics.fingerprint.FingerprintsEnrolledCategoryPreferenceController">
    </PreferenceCategory>

    <androidx.preference.Preference
        android:icon="@drawable/ic_add_24dp"
        android:key="key_fingerprint_add"
        android:title="@string/fingerprint_add_title" />

    <PreferenceCategory
        android:key="security_settings_fingerprint_unlock_category"
        android:title="@string/security_settings_fingerprint_settings_preferences_category"
        android:visibility="gone">

        <com.android.settingslib.RestrictedSwitchPreference
            android:key="security_settings_require_screen_on_to_auth"
            android:title="@string/security_settings_require_screen_on_to_auth_title"
            android:summary="@string/security_settings_require_screen_on_to_auth_description"
            settings:keywords="@string/security_settings_require_screen_on_to_auth_keywords"
            settings:controller="com.android.settings.biometrics.fingerprint.FingerprintSettingsRequireScreenOnToAuthPreferenceController" />
    </PreferenceCategory>

    <PreferenceCategory
        android:key="security_settings_fingerprint_footer">
    </PreferenceCategory>

</PreferenceScreen>
+207 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.biometrics.fingerprint2.domain.interactor

import android.content.Context
import android.content.Intent
import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.FingerprintManager.GenerateChallengeCallback
import android.hardware.fingerprint.FingerprintManager.RemovalCallback
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.os.CancellationSignal
import android.util.Log
import com.android.settings.biometrics.GatekeeperPasswordProvider
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
import com.android.settings.password.ChooseLockSettingsHelper
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext

private const val TAG = "FingerprintManagerInteractor"

/** Encapsulates business logic related to managing fingerprints. */
interface FingerprintManagerInteractor {
  /** Returns the list of current fingerprints. */
  val enrolledFingerprints: Flow<List<FingerprintViewModel>>

  /** Returns the max enrollable fingerprints, note during SUW this might be 1 */
  val maxEnrollableFingerprints: Flow<Int>

  /** Runs [FingerprintManager.authenticate] */
  suspend fun authenticate(): FingerprintAuthAttemptViewModel

  /**
   * Generates a challenge with the provided [gateKeeperPasswordHandle] and on success returns a
   * challenge and challenge token. This info can be used for secure operations such as
   * [FingerprintManager.enroll]
   *
   * @param gateKeeperPasswordHandle GateKeeper password handle generated by a Confirm
   * @return A [Pair] of the challenge and challenge token
   */
  suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray>

  /** Returns true if a user can enroll a fingerprint false otherwise. */
  fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean>

  /**
   * Removes the given fingerprint, returning true if it was successfully removed and false
   * otherwise
   */
  suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean

  /** Renames the given fingerprint if one exists */
  suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String)

  /** Indicates if the device has side fingerprint */
  suspend fun hasSideFps(): Boolean

  /** Indicates if the press to auth feature has been enabled */
  suspend fun pressToAuthEnabled(): Boolean

  /** Retrieves the sensor properties of a device */
  suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal>
}

class FingerprintManagerInteractorImpl(
  applicationContext: Context,
  private val backgroundDispatcher: CoroutineDispatcher,
  private val fingerprintManager: FingerprintManager,
  private val gatekeeperPasswordProvider: GatekeeperPasswordProvider,
  private val pressToAuthProvider: () -> Boolean,
) : FingerprintManagerInteractor {

  private val maxFingerprints =
    applicationContext.resources.getInteger(
      com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser
    )
  private val applicationContext = applicationContext.applicationContext

  override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> =
    suspendCoroutine {
      val callback = GenerateChallengeCallback { _, userId, challenge ->
        val intent = Intent()
        intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle)
        val challengeToken =
          gatekeeperPasswordProvider.requestGatekeeperHat(intent, challenge, userId)

        gatekeeperPasswordProvider.removeGatekeeperPasswordHandle(intent, false)
        val p = Pair(challenge, challengeToken)
        it.resume(p)
      }
      fingerprintManager.generateChallenge(applicationContext.userId, callback)
    }

  override val enrolledFingerprints: Flow<List<FingerprintViewModel>> = flow {
    emit(
      fingerprintManager
        .getEnrolledFingerprints(applicationContext.userId)
        .map { (FingerprintViewModel(it.name.toString(), it.biometricId, it.deviceId)) }
        .toList()
    )
  }

  override fun canEnrollFingerprints(numFingerprints: Int): Flow<Boolean> = flow {
    emit(numFingerprints < maxFingerprints)
  }

  override val maxEnrollableFingerprints = flow { emit(maxFingerprints) }

  override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean = suspendCoroutine {
    val callback =
      object : RemovalCallback() {
        override fun onRemovalError(
          fp: android.hardware.fingerprint.Fingerprint,
          errMsgId: Int,
          errString: CharSequence
        ) {
          it.resume(false)
        }

        override fun onRemovalSucceeded(
          fp: android.hardware.fingerprint.Fingerprint?,
          remaining: Int
        ) {
          it.resume(true)
        }
      }
    fingerprintManager.remove(
      android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId),
      applicationContext.userId,
      callback
    )
  }

  override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
    withContext(backgroundDispatcher) {
      fingerprintManager.rename(fp.fingerId, applicationContext.userId, newName)
    }
  }

  override suspend fun hasSideFps(): Boolean = suspendCancellableCoroutine {
    it.resume(fingerprintManager.isPowerbuttonFps)
  }

  override suspend fun pressToAuthEnabled(): Boolean = suspendCancellableCoroutine {
    it.resume(pressToAuthProvider())
  }

  override suspend fun sensorPropertiesInternal(): List<FingerprintSensorPropertiesInternal> =
    suspendCancellableCoroutine {
      it.resume(fingerprintManager.sensorPropertiesInternal)
    }

  override suspend fun authenticate(): FingerprintAuthAttemptViewModel =
    suspendCancellableCoroutine { c: CancellableContinuation<FingerprintAuthAttemptViewModel> ->
      val authenticationCallback =
        object : FingerprintManager.AuthenticationCallback() {

          override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
            super.onAuthenticationError(errorCode, errString)
            if (c.isCompleted) {
              Log.d(TAG, "framework sent down onAuthError after finish")
              return
            }
            c.resume(FingerprintAuthAttemptViewModel.Error(errorCode, errString.toString()))
          }

          override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
            super.onAuthenticationSucceeded(result)
            if (c.isCompleted) {
              Log.d(TAG, "framework sent down onAuthError after finish")
              return
            }
            c.resume(FingerprintAuthAttemptViewModel.Success(result.fingerprint?.biometricId ?: -1))
          }
        }

      val cancellationSignal = CancellationSignal()
      c.invokeOnCancellation { cancellationSignal.cancel() }
      fingerprintManager.authenticate(
        null,
        cancellationSignal,
        authenticationCallback,
        null,
        applicationContext.userId
      )
    }
}
+177 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.biometrics.fingerprint2.ui.binder

import android.hardware.fingerprint.FingerprintManager
import android.util.Log
import androidx.lifecycle.LifecycleCoroutineScope
import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintSettingsViewBinder.FingerprintView
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollAdditionalFingerprint
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintStateViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchedActivity
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.PreferenceViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch

private const val TAG = "FingerprintSettingsViewBinder"

/** Binds a [FingerprintSettingsViewModel] to a [FingerprintView] */
object FingerprintSettingsViewBinder {

  interface FingerprintView {
    /**
     * Helper function to launch fingerprint enrollment(This should be the default behavior when a
     * user enters their PIN/PATTERN/PASS and no fingerprints are enrolled).
     */
    fun launchFullFingerprintEnrollment(
      userId: Int,
      gateKeeperPasswordHandle: Long?,
      challenge: Long?,
      challengeToken: ByteArray?
    )

    /** Helper to launch an add fingerprint request */
    fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?)
    /**
     * Helper function that will try and launch confirm lock, if that fails we will prompt user to
     * choose a PIN/PATTERN/PASS.
     */
    fun launchConfirmOrChooseLock(userId: Int)

    /** Used to indicate that FingerprintSettings is finished. */
    fun finish()

    /** Indicates what result should be set for the returning callee */
    fun setResultExternal(resultCode: Int)
    /** Indicates the settings UI should be shown */
    fun showSettings(state: FingerprintStateViewModel)
    /** Indicates that a user has been locked out */
    fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error)
    /** Indicates a fingerprint preference should be highlighted */
    suspend fun highlightPref(fingerId: Int)
    /** Indicates a user should be prompted to delete a fingerprint */
    suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean
    /** Indicates a user should be asked to renae ma dialog */
    suspend fun askUserToRenameDialog(
      fingerprintViewModel: FingerprintViewModel
    ): Pair<FingerprintViewModel, String>?
  }

  fun bind(
    view: FingerprintView,
    viewModel: FingerprintSettingsViewModel,
    navigationViewModel: FingerprintSettingsNavigationViewModel,
    lifecycleScope: LifecycleCoroutineScope,
  ) {

    /** Result listener for launching enrollments **after** a user has reached the settings page. */

    // Settings display flow
    lifecycleScope.launch {
      viewModel.fingerprintState.filterNotNull().collect { view.showSettings(it) }
    }

    // Dialog flow
    lifecycleScope.launch {
      viewModel.isShowingDialog.collectLatest {
        if (it == null) {
          return@collectLatest
        }
        when (it) {
          is PreferenceViewModel.RenameDialog -> {
            val willRename = view.askUserToRenameDialog(it.fingerprintViewModel)
            if (willRename != null) {
              Log.d(TAG, "renaming fingerprint $it")
              viewModel.renameFingerprint(willRename.first, willRename.second)
            }
            viewModel.onRenameDialogFinished()
          }
          is PreferenceViewModel.DeleteDialog -> {
            if (view.askUserToDeleteDialog(it.fingerprintViewModel)) {
              Log.d(TAG, "deleting fingerprint $it")
              viewModel.deleteFingerprint(it.fingerprintViewModel)
            }
            viewModel.onDeleteDialogFinished()
          }
        }
      }
    }

    // Auth flow
    lifecycleScope.launch {
      viewModel.authFlow.filterNotNull().collect {
        when (it) {
          is FingerprintAuthAttemptViewModel.Success -> {
            view.highlightPref(it.fingerId)
          }
          is FingerprintAuthAttemptViewModel.Error -> {
            if (it.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
              view.userLockout(it)
            }
          }
        }
      }
    }

    // Launch this on Dispatchers.Default and not main.
    // Otherwise it takes too long for state transitions such as PIN/PATTERN/PASS
    // to enrollment, which makes gives the user a janky experience.
    lifecycleScope.launch(Dispatchers.Default) {
      var settingsShowingJob: Job? = null
      navigationViewModel.nextStep.filterNotNull().collect { nextStep ->
        settingsShowingJob?.cancel()
        settingsShowingJob = null
        Log.d(TAG, "next step = $nextStep")
        when (nextStep) {
          is EnrollFirstFingerprint ->
            view.launchFullFingerprintEnrollment(
              nextStep.userId,
              nextStep.gateKeeperPasswordHandle,
              nextStep.challenge,
              nextStep.challengeToken
            )
          is EnrollAdditionalFingerprint ->
            view.launchAddFingerprint(nextStep.userId, nextStep.challengeToken)
          is LaunchConfirmDeviceCredential -> view.launchConfirmOrChooseLock(nextStep.userId)
          is FinishSettings -> {
            Log.d(TAG, "Finishing due to ${nextStep.reason}")
            view.finish()
          }
          is FinishSettingsWithResult -> {
            Log.d(TAG, "Finishing with result ${nextStep.result} due to ${nextStep.reason}")
            view.setResultExternal(nextStep.result)
            view.finish()
          }
          is ShowSettings -> Log.d(TAG, "Showing settings")
          is LaunchedActivity -> Log.d(TAG, "Launched activity, awaiting result")
        }
      }
    }
  }
}
+0 −128
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.biometrics.fingerprint2.ui.binder

import androidx.lifecycle.LifecycleCoroutineScope
import com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollAdditionalFingerprint
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch

/**
 * Binds a [FingerprintSettingsViewModel] to a [FingerprintSettingsV2Fragment]
 */
object FingerprintViewBinder {

    interface Binding {
        fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?)
        fun onEnrollSuccess()
        fun onEnrollAdditionalFailure()
        fun onEnrollFirstFailure(reason: String)
        fun onEnrollFirstFailure(reason: String, resultCode: Int)
        fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?)
    }

    /** Initial listener for the first enrollment request */
    fun bind(
        viewModel: FingerprintSettingsViewModel,
        lifecycleScope: LifecycleCoroutineScope,
        token: ByteArray?,
        challenge: Long?,
        launchFullFingerprintEnrollment: (
            userId: Int,
            gateKeeperPasswordHandle: Long?,
            challenge: Long?,
            challengeToken: ByteArray?
        ) -> Unit,
        launchAddFingerprint: (userId: Int, challengeToken: ByteArray?) -> Unit,
        launchConfirmOrChooseLock: (userId: Int) -> Unit,
        finish: () -> Unit,
        setResultExternal: (resultCode: Int) -> Unit,
    ): Binding {

        lifecycleScope.launch {
            viewModel.nextStep.filterNotNull().collect { nextStep ->
                when (nextStep) {
                    is EnrollFirstFingerprint -> launchFullFingerprintEnrollment(
                        nextStep.userId,
                        nextStep.gateKeeperPasswordHandle,
                        nextStep.challenge,
                        nextStep.challengeToken
                    )

                    is EnrollAdditionalFingerprint -> launchAddFingerprint(
                        nextStep.userId, nextStep.challengeToken
                    )

                    is LaunchConfirmDeviceCredential -> launchConfirmOrChooseLock(nextStep.userId)

                    is FinishSettings -> {
                        println("Finishing due to ${nextStep.reason}")
                        finish()
                    }

                    is FinishSettingsWithResult -> {
                        println("Finishing with result ${nextStep.result} due to ${nextStep.reason}")
                        setResultExternal(nextStep.result)
                        finish()
                    }

                    is ShowSettings -> println("show settings")
                }

                viewModel.onUiCommandExecuted()
            }
        }

        viewModel.updateTokenAndChallenge(token, if (challenge == -1L) null else challenge)

        return object : Binding {
            override fun onConfirmDevice(
                wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?
            ) {
                viewModel.onConfirmDevice(wasSuccessful, theGateKeeperPasswordHandle)
            }

            override fun onEnrollSuccess() {
                viewModel.onEnrollSuccess()
            }

            override fun onEnrollAdditionalFailure() {
                viewModel.onEnrollAdditionalFailure()
            }

            override fun onEnrollFirstFailure(reason: String) {
                viewModel.onEnrollFirstFailure(reason)
            }

            override fun onEnrollFirstFailure(reason: String, resultCode: Int) {
                viewModel.onEnrollFirstFailure(reason, resultCode)
            }

            override fun onEnrollFirst(token: ByteArray?, keyChallenge: Long?) {
                viewModel.onEnrollFirst(token, keyChallenge)
            }
        }
    }

}
 No newline at end of file
+119 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading