DeviceUtils.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.content.Context;
import android.os.Build;

import androidx.annotation.NonNull;

/**
 * Utility class for specifying custom behavior based on the vendor and model of the device.
 */
class DeviceUtils {
    // Prevent instantiation.
    private DeviceUtils() {}

    /**
     * Checks if the current device should explicitly fall back to using
     * {@link FingerprintDialogFragment} 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 Whether the current device should fall back to fingerprint for crypto-based
     *  authentication.
     */
    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);
    }

    /**
     * Checks if the current device should hide {@link FingerprintDialogFragment} and ensure that
     * {@link FingerprintDialogFragment} is always dismissed immediately upon receiving an error or
     * cancel signal (e.g. if the dialog is shown behind an overlay).
     *
     * @param context The application or activity context.
     * @param model Model name of the current device.
     * @return Whether the {@link FingerprintDialogFragment} should be hidden.
     */
    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);
    }

    /**
     * Checks if the current device should delay showing a new biometric prompt when the previous
     * prompt was recently dismissed.
     *
     * @param context The application or activity context.
     * @param model Model name of the current device.
     * @return Whether showing the prompt should be delayed after dismissal.
     */
    static boolean shouldDelayShowingPrompt(@NonNull Context context, String model) {
        if (Build.VERSION.SDK_INT != Build.VERSION_CODES.Q) {
            // This workaround is only needed for API 29.
            return false;
        }
        return isModelInList(context, model, R.array.delay_showing_prompt_models);
    }

    /**
     * Checks if all biometric sensors for the current device can be assumed to meet the
     * <strong>Class 3</strong> (formerly <strong>Strong</strong>) security threshold.
     *
     * @param context The application or activity context.
     * @param model Model name of the current device.
     * @return Whether the device can be assumed to have only <strong>Class 3</strong> biometrics.
     */
    static boolean canAssumeStrongBiometrics(@NonNull Context context, String model) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            // Android 11 (API 30) and above may downgrade a sensor's security class at runtime.
            return false;
        }
        return isModelInList(context, model, R.array.assume_strong_biometrics_models);
    }

    /**
     * Checks 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 Whether the vendor name matches one in the string array.
     */
    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;
    }

    /**
     * Checks 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 Whether the model matches a prefix in the string array.
     */
    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;
    }

    /**
     * Checks if the current device model matches one 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 Whether the model matches one in the string array.
     */
    private static boolean isModelInList(@NonNull Context context, String model, int resId) {
        if (model == null) {
            return false;
        }

        final String[] modelNames = context.getResources().getStringArray(resId);
        for (final String modelName : modelNames) {
            if (model.equals(modelName)) {
                return true;
            }
        }
        return false;
    }
}