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

Commit 76237177 authored by Joshua McCloskey's avatar Joshua McCloskey
Browse files

Adding more tests for FingerprintSettingsV2

Test: atest FingerprintSettingsViewModelTest
FingerprintSettingsNavigationModelTest
Bug: 280862076

Change-Id: Ibb3d0112f394d6776fc1b346d226d9f7720cfed8
parent d7348251
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

@@ -49,7 +50,13 @@ class FingerprintSettingsNavigationViewModel(
    if (challengeInit == null || tokenInit == null) {
      _nextStep.update { LaunchConfirmDeviceCredential(userId) }
    } else {
      viewModelScope.launch { showSettingsHelper() }
      viewModelScope.launch {
        if (fingerprintManagerInteractor.enrolledFingerprints.last().isEmpty()) {
          _nextStep.update { EnrollFirstFingerprint(userId, null, challenge, token) }
        } else {
          showSettingsHelper()
        }
      }
    }
  }

+26 −22
Original line number Diff line number Diff line
@@ -17,8 +17,7 @@
package com.android.settings.biometrics.fingerprint2.ui.viewmodel

import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.util.Log
import androidx.lifecycle.ViewModel
@@ -67,24 +66,6 @@ class FingerprintSettingsViewModel(
      }
    }

  init {
    viewModelScope.launch {
      fingerprintSensorPropertiesInternal.update {
        fingerprintManagerInteractor.sensorPropertiesInternal()
      }
    }

    viewModelScope.launch {
      navigationViewModel.nextStep.filterNotNull().collect {
        _isShowingDialog.update { null }
        if (it is ShowSettings) {
          // reset state
          updateSettingsData()
        }
      }
    }
  }

  private val _fingerprintStateViewModel: MutableStateFlow<FingerprintStateViewModel?> =
    MutableStateFlow(null)
  val fingerprintState: Flow<FingerprintStateViewModel?> =
@@ -103,7 +84,6 @@ class FingerprintSettingsViewModel(
    MutableSharedFlow()

  private val attemptsSoFar: MutableStateFlow<Int> = MutableStateFlow(0)

  /**
   * This is a very tricky flow. The current fingerprint manager APIs are not robust, and a proper
   * implementation would take quite a lot of code to implement, it might be easier to rewrite
@@ -139,7 +119,13 @@ class FingerprintSettingsViewModel(
          return@combine false
        }
        val sensorType = sensorProps[0].sensorType
        if (listOf(TYPE_UDFPS_OPTICAL, TYPE_UDFPS_ULTRASONIC).contains(sensorType)) {
        if (
          listOf(
              FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
              FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC
            )
            .contains(sensorType)
        ) {
          return@combine false
        }

@@ -182,6 +168,24 @@ class FingerprintSettingsViewModel(
      }
      .flowOn(backgroundDispatcher)

  init {
    viewModelScope.launch {
      fingerprintSensorPropertiesInternal.update {
        fingerprintManagerInteractor.sensorPropertiesInternal()
      }
    }

    viewModelScope.launch {
      navigationViewModel.nextStep.filterNotNull().collect {
        _isShowingDialog.update { null }
        if (it is ShowSettings) {
          // reset state
          updateSettingsData()
        }
      }
    }
  }

  /** The rename dialog has finished */
  fun onRenameDialogFinished() {
    _isShowingDialog.update { null }
+5 −1
Original line number Diff line number Diff line
@@ -67,7 +67,11 @@ class FakeFingerprintManagerInteractor : FingerprintManagerInteractor {
    return enrolledFingerprintsInternal.remove(fp)
  }

  override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {}
  override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
    if (enrolledFingerprintsInternal.remove(fp)) {
      enrolledFingerprintsInternal.add(FingerprintViewModel(newName, fp.fingerId, fp.deviceId))
    }
  }

  override suspend fun hasSideFps(): Boolean {
    return sensorProps.any { it.isAnySidefpsType }
+94 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.settings.fingerprint2.viewmodel

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.android.settings.biometrics.BiometricEnrollBase
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
@@ -272,4 +273,97 @@ class FingerprintSettingsNavigationViewModelTest {
      assertThat(nextStep).isEqualTo(ShowSettings)
      job.cancel()
    }

  @Test
  fun enrollWithToken_andNoUsers_startsFingerprintEnrollment() =
    testScope.runTest {
      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()

      var nextStep: NextStepViewModel? = null
      val job = launch { underTest.nextStep.collect { nextStep = it } }

      val token = byteArrayOf(1)
      val challenge = 5L

      underTest =
        FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
            defaultUserId,
            fakeFingerprintManagerInteractor,
            backgroundDispatcher,
            token,
            challenge,
          )
          .create(FingerprintSettingsNavigationViewModel::class.java)

      runCurrent()

      assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, null, challenge, token))
      job.cancel()
    }

  @Test
  fun enroll_shouldNotFinish() =
    testScope.runTest {
      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf()

      var nextStep: NextStepViewModel? = null
      val job = launch { underTest.nextStep.collect { nextStep = it } }

      val token = byteArrayOf(1)
      val challenge = 5L

      underTest =
        FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
            defaultUserId,
            fakeFingerprintManagerInteractor,
            backgroundDispatcher,
            token,
            challenge,
          )
          .create(FingerprintSettingsNavigationViewModel::class.java)

      runCurrent()

      assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, null, challenge, token))
      underTest.maybeFinishActivity(false)

      runCurrent()
      assertThat(nextStep).isEqualTo(EnrollFirstFingerprint(defaultUserId, null, challenge, token))
      job.cancel()
    }

  @Test
  fun showSettings_shouldFinish() =
    testScope.runTest {
      fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
        mutableListOf(FingerprintViewModel("a", 1, 3L))

      var nextStep: NextStepViewModel? = null
      val job = launch { underTest.nextStep.collect { nextStep = it } }

      val token = byteArrayOf(1)
      val challenge = 5L

      underTest =
        FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
            defaultUserId,
            fakeFingerprintManagerInteractor,
            backgroundDispatcher,
            token,
            challenge,
          )
          .create(FingerprintSettingsNavigationViewModel::class.java)

      runCurrent()
      assertThat(nextStep).isEqualTo(ShowSettings)

      underTest.maybeFinishActivity(false)

      runCurrent()
      assertThat(nextStep)
        .isEqualTo(
          FinishSettingsWithResult(BiometricEnrollBase.RESULT_TIMEOUT, "onStop finishing settings")
        )
      job.cancel()
    }
}
+168 −1
Original line number Diff line number Diff line
@@ -213,7 +213,8 @@ class FingerprintSettingsViewModelTest {
  @Test
  fun deleteDialog_showAndDismiss() = runTest {
    val fingerprintToDelete = FingerprintViewModel("A", 1, 10L)
    fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = mutableListOf(fingerprintToDelete)
    fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
      mutableListOf(fingerprintToDelete)

    underTest =
      FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
@@ -244,4 +245,170 @@ class FingerprintSettingsViewModelTest {

    dialogJob.cancel()
  }

  @Test
  fun renameDialog_showAndDismiss() = runTest {
    val fingerprintToRename = FingerprintViewModel("World", 1, 10L)
    fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
      mutableListOf(fingerprintToRename)

    underTest =
      FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
          defaultUserId,
          fakeFingerprintManagerInteractor,
          backgroundDispatcher,
          navigationViewModel,
        )
        .create(FingerprintSettingsViewModel::class.java)

    var dialog: PreferenceViewModel? = null
    val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } }

    // Move to the ShowSettings state
    navigationViewModel.onConfirmDevice(true, 10L)
    runCurrent()
    underTest.onPrefClicked(fingerprintToRename)
    runCurrent()

    assertThat(dialog is PreferenceViewModel.DeleteDialog)
    assertThat(dialog).isEqualTo(PreferenceViewModel.RenameDialog(fingerprintToRename))

    underTest.renameFingerprint(fingerprintToRename, "Hello")
    underTest.onRenameDialogFinished()
    runCurrent()

    assertThat(dialog).isNull()
    assertThat(fakeFingerprintManagerInteractor.enrolledFingerprintsInternal.first().name)
      .isEqualTo("Hello")

    dialogJob.cancel()
  }

  @Test
  fun testTwoDialogsCannotShow_atSameTime() = runTest {
    val fingerprintToDelete = FingerprintViewModel("A", 1, 10L)
    fakeFingerprintManagerInteractor.enrolledFingerprintsInternal =
      mutableListOf(fingerprintToDelete)

    underTest =
      FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
          defaultUserId,
          fakeFingerprintManagerInteractor,
          backgroundDispatcher,
          navigationViewModel,
        )
        .create(FingerprintSettingsViewModel::class.java)

    var dialog: PreferenceViewModel? = null
    val dialogJob = launch { underTest.isShowingDialog.collect { dialog = it } }

    // Move to the ShowSettings state
    navigationViewModel.onConfirmDevice(true, 10L)
    runCurrent()
    underTest.onDeleteClicked(fingerprintToDelete)
    runCurrent()

    assertThat(dialog is PreferenceViewModel.DeleteDialog)
    assertThat(dialog).isEqualTo(PreferenceViewModel.DeleteDialog(fingerprintToDelete))

    underTest.onPrefClicked(fingerprintToDelete)
    runCurrent()
    assertThat(dialog is PreferenceViewModel.DeleteDialog)
    assertThat(dialog).isEqualTo(PreferenceViewModel.DeleteDialog(fingerprintToDelete))

    dialogJob.cancel()
  }

  @Test
  fun authenticatePauses_whenPaused() =
    testScope.runTest {
      val fingerprints = setupAuth()
      val success = FingerprintAuthAttemptViewModel.Success(fingerprints.first().fingerId)

      var authAttempt: FingerprintAuthAttemptViewModel? = null

      val job = launch { underTest.authFlow.take(5).collectLatest { authAttempt = it } }

      underTest.shouldAuthenticate(true)
      navigationViewModel.onConfirmDevice(true, 10L)

      advanceTimeBy(400)
      runCurrent()
      assertThat(authAttempt).isEqualTo(success)

      fakeFingerprintManagerInteractor.authenticateAttempt =
        FingerprintAuthAttemptViewModel.Success(10)
      underTest.shouldAuthenticate(false)
      advanceTimeBy(400)
      runCurrent()

      // The most recent auth attempt shouldn't have changed.
      assertThat(authAttempt).isEqualTo(success)
      job.cancel()
    }

  @Test
  fun dialog_pausesAuth() =
    testScope.runTest {
      val fingerprints = setupAuth()

      var authAttempt: FingerprintAuthAttemptViewModel? = null
      val job = launch { underTest.authFlow.take(1).collectLatest { authAttempt = it } }
      underTest.shouldAuthenticate(true)
      navigationViewModel.onConfirmDevice(true, 10L)

      underTest.onPrefClicked(fingerprints[0])
      advanceTimeBy(400)

      job.cancel()
      assertThat(authAttempt).isEqualTo(null)
    }

  @Test
  fun cannotAuth_when_notShowingSettings() =
    testScope.runTest {
      val fingerprints = setupAuth()

      var authAttempt: FingerprintAuthAttemptViewModel? = null
      val job = launch { underTest.authFlow.take(1).collectLatest { authAttempt = it } }
      underTest.shouldAuthenticate(true)
      navigationViewModel.onConfirmDevice(true, 10L)

      // This should cause the state to change to FingerprintEnrolling
      navigationViewModel.onAddFingerprintClicked()
      advanceTimeBy(400)

      job.cancel()
      assertThat(authAttempt).isEqualTo(null)
    }

  private fun setupAuth(): MutableList<FingerprintViewModel> {
    fakeFingerprintManagerInteractor.sensorProps =
      listOf(
        FingerprintSensorPropertiesInternal(
          0 /* sensorId */,
          SensorProperties.STRENGTH_STRONG,
          5 /* maxEnrollmentsPerUser */,
          emptyList() /* ComponentInfoInternal */,
          FingerprintSensorProperties.TYPE_POWER_BUTTON,
          true /* resetLockoutRequiresHardwareAuthToken */
        )
      )
    val fingerprints =
      mutableListOf(FingerprintViewModel("a", 1, 3L), FingerprintViewModel("b", 2, 5L))
    fakeFingerprintManagerInteractor.enrolledFingerprintsInternal = fingerprints
    val success = FingerprintAuthAttemptViewModel.Success(1)
    fakeFingerprintManagerInteractor.authenticateAttempt = success

    underTest =
      FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
          defaultUserId,
          fakeFingerprintManagerInteractor,
          backgroundDispatcher,
          navigationViewModel,
        )
        .create(FingerprintSettingsViewModel::class.java)

    return fingerprints
  }
}