Repository: flutter
Issue: [local_auth] [android] get 'not_enrolled' for locked_out times after first get 'locked_out' by too many failed attempts on Android
Labels: platform-android,p: local_auth,package,has reproducible steps,P3,team-android,triaged-android,found in release: 3.22

### What package does this bug report belong to?

local_auth

### What target platforms are you seeing this bug on?

Android

### Have you already upgraded your packages?

Yes

### Dependency versions

<details><summary>pubspec.lock</summary>

  local_auth:
    dependency: "direct main"
    description:
      path: "packages/local_auth/local_auth"
      ref: HEAD
      resolved-ref: "86dfd4aa35c7892478317932208115e5e151c889"
      url: "https://github.com/AKAPUCH/packages"
    source: git
    version: "2.2.0"
  local_auth_android:
    dependency: transitive
    description:
      path: "packages/local_auth/local_auth_android"
      ref: HEAD
      resolved-ref: "86dfd4aa35c7892478317932208115e5e151c889"
      url: "https://github.com/AKAPUCH/packages"
    source: git
    version: "1.0.38"
  local_auth_darwin:
    dependency: transitive
    description:
      path: "packages/local_auth/local_auth_darwin"
      ref: HEAD
      resolved-ref: "86dfd4aa35c7892478317932208115e5e151c889"
      url: "https://github.com/AKAPUCH/packages"
    source: git
    version: "1.2.2"
  local_auth_platform_interface:
    dependency: transitive
    description:
      name: local_auth_platform_interface
      sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a"
      url: "https://pub.dev"
    source: hosted
    version: "1.0.10"
  local_auth_windows:
    dependency: transitive
    description:
      name: local_auth_windows
      sha256: "505ba3367ca781efb1c50d3132e44a2446bccc4163427bc203b9b4d8994d97ea"
      url: "https://pub.dev"
    source: hosted
    version: "1.0.10"

</details>


### Steps to reproduce

I tried to make flutter app using local_auth packages in Samsung Galaxy S20 5G.
my logic is:

1. call `canCheckBiometrics` to check whether device can authenticate. if not, emit error.
2. call `getAvailableBiometrics` to check whether device has biometrics. if not, also emit error.
3. after 2 steps, call `authenticate` to try authenticate.

I wanted to test whether local_auth still gives 'locked_out' exception after first get 'locked_out' exception by too many attempts.
Unfortunately, I got 'not_enrolled' exception... I'm wondering whether this error handling logic is provided by android(biometric_manager, biometric_prompt I guess..) or not.

### Expected results

get 'locked_out' exception for locked_out times after first get 'locked_out' exception by too many failed attempts.

### Actual results

get 'not_enrolled' exception for locked_out times after first get 'locked_out' exception by too many failed attempts.

### Code sample

<details open><summary>Code sample</summary>

```dart
class BaseException {
  final String code;
  final String message;
  final String? errorDetail;

  const BaseException(
      {required this.code, required this.message, required this.errorDetail});
}

import 'package:freezed_annotation/freezed_annotation.dart';

part 'bio_auth_exception.freezed.dart';

@freezed
class BioAuthException with _$BioAuthException implements BaseException {
  const factory BioAuthException.failToExecute(
      {@Default('99') String code,
      @Default('플러그인 실행에 실패했습니다.') String message,
      String? errorDetail}) = FailToExecute;
  const factory BioAuthException.permissionDenied(
      {@Default('98') String code,
      @Default('생체인증을 사용할 수 없습니다. 권한을 확인해주세요.') String message,
      String? errorDetail}) = PermissionDenied;
  const factory BioAuthException.noBiometricRegistered(
      {@Default('97') String code,
      @Default('생체인증이 등록되지 않은 단말입니다.') String message,
      String? errorDetail}) = NoBiometricRegistered;
  const factory BioAuthException.exceededLimit(
      {@Default('96') String code,
      @Default('생체인증 최대 횟수를 초과하였습니다.') String message,
      String? errorDetail}) = ExceededLimit;
  const factory BioAuthException.notSupported(
      {@Default('95') String code,
      @Default('생체인증을 지원하지 않는 단말입니다.') String message,
      String? errorDetail}) = NotSupported;
  const factory BioAuthException.userCanceled(
      {@Default('90') String code,
      @Default('사용자가 실행을 취소했습니다.') String message,
      String? errorDetail}) = UserCanceled;
}

/// in another file
import '.../features/bio_auth/domain/error/bio_auth_exception.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:local_auth/local_auth.dart';
import 'package:local_auth_android/local_auth_android.dart';
import 'package:local_auth_darwin/local_auth_darwin.dart';
import 'package:rxdart_ext/rxdart_ext.dart';

abstract class BioAuthRepository {
  Single<void> registerBioAuth();
}

class BioAuthRepositoryImpl extends BioAuthRepository {
  final LocalAuthentication authentication = LocalAuthentication();

  @override
  Single<void> registerBioAuth() {
    return Single.fromFuture(checkCapability()).flatMapSingle<bool>(
      (isCapable) {
        return isCapable
            ? Single.fromFuture(checkBiometricExists())
            : Single.error(const NotSupported());
      },
    ).flatMapSingle<bool>((bioExists) {
      return bioExists
          ? Single.fromFuture(doAuthenticate())
          : Single.error(const NoBiometricRegistered());
    }).onErrorResumeSingle((e, stackTrace) {
      if (e is PlatformException) {
        switch (e.code) {
          case 'NotAvailable':
            return Single.error(const PermissionDenied());
          case 'NotEnrolled':
          case 'PasscodeNotSet':
            return Single.error(const NoBiometricRegistered());
          case 'UserCancel':
            return Single.error(const UserCanceled());
          case 'LockedOut':
          case 'PermanentlyLockedOut':
            return Single.error(const ExceededLimit());
        }
        return Single.error(FailToExecute(errorDetail: e.message));
      } else if (e is BaseException) {
        return Single.error(e);
      }
      return Single.error(FailToExecute(errorDetail: e.toString()));
    });
  }

  Future<bool> checkCapability() async {
    return authentication.canCheckBiometrics;
  }

  Future<bool> checkBiometricExists() async {
    return (await authentication.getAvailableBiometrics()).isNotEmpty;
  }

  Future<bool> doAuthenticate() async {
    return await authentication.authenticate(
      authMessages: <AuthMessages>[
        const IOSAuthMessages(localizedFallbackTitle: '확인'),
        AndroidAuthMessages(
          signInTitle: 'androidSignInTitle'.tr(),
          cancelButton: 'androidCancelButton'.tr(),
          biometricHint: ' ',
        ),
      ],
      localizedReason: 'localizedReason'.tr(),
      options: const AuthenticationOptions(
        useErrorDialogs: false,
        stickyAuth: true,
        biometricOnly: true,
        sensitiveTransaction: true,
      ),
    );
  }
}

```

</details>


### Screenshots or Videos

<details open>
<summary>Screenshots / Video demonstration</summary>

[Upload media here]

</details>


### Logs

<details open><summary>Logs</summary>

```console
# fail attempts on purpose
I/BiometricPrompt(11181): onAuthenticationFailed
I/BiometricPrompt(11181): onAuthenticationFailed
I/BiometricPrompt(11181): onAuthenticationFailed
I/BiometricPrompt(11181): onAuthenticationFailed
I/BiometricPrompt(11181): onError: 7, 0
# after retry
I/BiometricPrompt(11181): onError: 11, 0
```

</details>


### Flutter Doctor output

<details open><summary>Doctor output</summary>

```console
[✓] Flutter (Channel main, 3.22.0-33.0.pre.3, on macOS 14.3.1 23D60 darwin-arm64, locale ko-KR)
    • Flutter version 3.22.0-33.0.pre.3 on channel main at /Users/a84931/development/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 1484d09103 (9일 전), 2024-05-13 21:58:26 -0400
    • Engine revision a1cdcb6a66
    • Dart version 3.5.0 (build 3.5.0-151.0.dev)
    • DevTools version 2.36.0-dev.5

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/a84931/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • ANDROID_HOME = /Users/a84931/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15C500b
    • CocoaPods version 1.15.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)

[✓] VS Code (version 1.89.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.88.0

[✓] Connected device (7 available)
    • SM G981N (mobile)               • R3CN60BDEXZ                          • android-arm64  • Android 13 (API 33)
    • iPhone 15 (mobile)              • 9357BC41-7501-4205-923A-46DCBECE8302 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-17-2 (simulator)
    • macOS (desktop)                 • macos                                • darwin-arm64   • macOS 14.3.1 23D60 darwin-arm64
    • Mac Designed for iPad (desktop) • mac-designed-for-ipad                • darwin         • macOS 14.3.1 23D60 darwin-arm64
    • Chrome (web)                    • chrome                               • web-javascript • Google Chrome 125.0.6422.76

[✓] Network resources
    • All expected network resources are available.

• No issues found!

```

</details>