Utils.java

/*
 * Copyright 2019 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 androidx.biometric;

import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.fragment.app.FragmentActivity;

/**
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY)
class Utils {
    // Private constructor to prevent instantiation.
    private Utils() {
    }

    /**
     * Determines if the given ID fails to match any known error message.
     *
     * @param errMsgId Integer ID representing an error.
     * @return true if the error is not publicly defined, or false otherwise.
     */
    static boolean isUnknownError(int errMsgId) {
        switch (errMsgId) {
            case BiometricPrompt.ERROR_HW_UNAVAILABLE:
            case BiometricPrompt.ERROR_UNABLE_TO_PROCESS:
            case BiometricPrompt.ERROR_TIMEOUT:
            case BiometricPrompt.ERROR_NO_SPACE:
            case BiometricPrompt.ERROR_CANCELED:
            case BiometricPrompt.ERROR_LOCKOUT:
            case BiometricPrompt.ERROR_VENDOR:
            case BiometricPrompt.ERROR_LOCKOUT_PERMANENT:
            case BiometricPrompt.ERROR_USER_CANCELED:
            case BiometricPrompt.ERROR_NO_BIOMETRICS:
            case BiometricPrompt.ERROR_HW_NOT_PRESENT:
            case BiometricPrompt.ERROR_NEGATIVE_BUTTON:
            case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL:
                return false;
            default:
                return true;
        }
    }

    /**
     * Launches the confirm device credential (CDC) Settings activity to allow the user to
     * authenticate with their device credential (PIN/pattern/password) on Android P and below.
     *
     * @param loggingTag The tag to be used for logging events.
     * @param activity Activity that will launch the CDC activity and handle its result. Should be
     *                 {@link DeviceCredentialHandlerActivity}; all other activities will fail to
     *                 launch the CDC activity and instead log an error.
     * @param bundle Bundle of extras forwarded from {@link BiometricPrompt}.
     * @param onLaunch Optional callback to be run before launching the new activity.
     */
    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    static void launchDeviceCredentialConfirmation(
            @NonNull String loggingTag, @Nullable FragmentActivity activity,
            @Nullable Bundle bundle, @Nullable Runnable onLaunch) {
        if (!(activity instanceof DeviceCredentialHandlerActivity)) {
            Log.e(loggingTag, "Failed to check device credential. Parent handler not found.");
            return;
        }
        final DeviceCredentialHandlerActivity handlerActivity =
                (DeviceCredentialHandlerActivity) activity;

        // Get the KeyguardManager service in whichever way the platform supports.
        final KeyguardManager keyguardManager;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            keyguardManager = handlerActivity.getSystemService(KeyguardManager.class);
        } else {
            final Object service = handlerActivity.getSystemService(Context.KEYGUARD_SERVICE);
            if (!(service instanceof KeyguardManager)) {
                Log.e(loggingTag, "Failed to check device credential. KeyguardManager not found.");
                handlerActivity.handleDeviceCredentialResult(Activity.RESULT_CANCELED);
                return;
            }
            keyguardManager = (KeyguardManager) service;
        }

        if (keyguardManager == null) {
            Log.e(loggingTag, "Failed to check device credential. KeyguardManager was null.");
            handlerActivity.handleDeviceCredentialResult(Activity.RESULT_CANCELED);
            return;
        }

        // Pass along the title and subtitle from the biometric prompt.
        final CharSequence title;
        final CharSequence subtitle;
        if (bundle != null) {
            title = bundle.getCharSequence(BiometricPrompt.KEY_TITLE);
            subtitle = bundle.getCharSequence(BiometricPrompt.KEY_SUBTITLE);
        } else {
            title = null;
            subtitle = null;
        }

        @SuppressWarnings("deprecation")
        final Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(title, subtitle);
        if (intent == null) {
            Log.e(loggingTag, "Failed to check device credential. Got null intent from Keyguard.");
            handlerActivity.handleDeviceCredentialResult(Activity.RESULT_CANCELED);
            return;
        }

        // Prevent the bridge from resetting until the confirmation activity finishes.
        final DeviceCredentialHandlerBridge bridge = DeviceCredentialHandlerBridge.getInstance();
        bridge.setConfirmingDeviceCredential(true);
        bridge.startIgnoringReset();

        // Run callback after the CDC flag is set but before launching the activity.
        if (onLaunch != null) {
            onLaunch.run();
        }

        // Launch a new instance of the confirm device credential Settings activity.
        intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
        handlerActivity.startActivityForResult(intent, 0 /* requestCode */);
    }

    /**
     * Finishes a given activity if and only if it's a {@link DeviceCredentialHandlerActivity}.
     *
     * @param activity The activity to finish.
     */
    static void maybeFinishHandler(@Nullable FragmentActivity activity) {
        if (activity instanceof DeviceCredentialHandlerActivity && !activity.isFinishing()) {
            activity.finish();
        }
    }

    /**
<<<<<<< HEAD
     * Determines if the current device should explicitly fall back to using
     * {@link FingerprintDialogFragment} and {@link FingerprintHelperFragment} when
     * {@link BiometricPrompt#authenticate(BiometricPrompt.PromptInfo,
     * BiometricPrompt.CryptoObject)} is called.
     *
     * @param context The application or activity context.
     * @param vendor Name of the device vendor/manufacturer.
     * @param model Model name of the current device.
     * @return true if the current device should fall back to fingerprint for crypto-based
     * authentication, or false otherwise.
     */
    static boolean shouldUseFingerprintForCrypto(@NonNull Context context, String vendor,
            String model) {
        if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) {
            // This workaround is only needed for API 28.
            return false;
        }
        return isVendorInList(context, vendor, R.array.crypto_fingerprint_fallback_vendors)
            || isModelInPrefixList(context, model, R.array.crypto_fingerprint_fallback_prefixes);
    }

    /**
     * Determines if the current device requires {@link FingerprintDialogFragment} to always be
     * dismissed immediately upon receiving an error or cancel signal (e.g., if the dialog is
     * shown behind an overlay that sends a cancel signal when it is dismissed).
     *
     * @param context The application or activity context.
     * @param model Model name of the current device.
     * @return true if {@link FingerprintDialogFragment} should always be dismissed immediately, or
     * false otherwise.
     */
    static boolean shouldHideFingerprintDialog(@NonNull Context context, String model) {
        if (Build.VERSION.SDK_INT != Build.VERSION_CODES.P) {
            // This workaround is only needed for API 28.
            return false;
        }
        return isModelInPrefixList(context, model, R.array.hide_fingerprint_instantly_prefixes);
    }

    /**
     * Determines if the name of the current device vendor matches one in the given string array
     * resource.
     *
     * @param context The application or activity context.
     * @param vendor Case-insensitive name of the device vendor.
     * @param resId Resource ID for the string array of vendor names to check against.
     * @return true if the vendor name matches one in the given string array, or false otherwise.
     */
    private static boolean isVendorInList(@NonNull Context context, String vendor, int resId) {
        if (vendor == null) {
            return false;
        }

        final String[] vendorNames = context.getResources().getStringArray(resId);
        for (final String vendorName : vendorNames) {
            if (vendor.equalsIgnoreCase(vendorName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determines if the current device model matches a prefix in the given string array resource.
     *
     * @param context The application or activity context.
     * @param model Model name of the current device.
     * @param resId Resource ID for the string array of device model prefixes to check against.
     * @return true if the model matches one in the given string array, or false otherwise.
     */
    private static boolean isModelInPrefixList(@NonNull Context context, String model, int resId) {
        if (model == null) {
            return false;
        }

        final String[] modelPrefixes = context.getResources().getStringArray(resId);
        for (final String modelPrefix : modelPrefixes) {
            if (model.startsWith(modelPrefix)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determines if we are in the process of having the user confirm their PIN/pattern/password.
     *
     * @return true if the user is confirming their device credential, or false otherwise.
     */
    static boolean isConfirmingDeviceCredential() {
        DeviceCredentialHandlerBridge bridge = DeviceCredentialHandlerBridge.getInstanceIfNotNull();
        return bridge != null && bridge.isConfirmingDeviceCredential();
    }
}