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

Commit b7aade5c authored by Austin Delgado's avatar Austin Delgado
Browse files

Add addFallbackOption API

Test: atest BiometricPromptRequestTest
API-Coverage-Bug: 416069294
Bug: 391633641
Flag: android.hardware.biometrics.add_fallback
NO_IFTTT=PromptInfo updated

Change-Id: I781650703a2423bb2285239c9791c5d60a8e3a62
parent 9b4c9ccf
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
@@ -19425,6 +19425,14 @@ package android.hardware.biometrics {
    field @FlaggedApi("android.hardware.biometrics.identity_check_api") public static final int IDENTITY_CHECK = 65536; // 0x10000
  }
  @FlaggedApi("android.hardware.biometrics.add_fallback") public static interface BiometricManager.IconType {
    field @FlaggedApi("android.hardware.biometrics.add_fallback") public static final int ACCOUNT = 2; // 0x2
    field @FlaggedApi("android.hardware.biometrics.add_fallback") public static final int GENERIC = 3; // 0x3
    field @FlaggedApi("android.hardware.biometrics.add_fallback") public static final int PASSWORD = 0; // 0x0
    field @FlaggedApi("android.hardware.biometrics.add_fallback") public static final int QR_CODE = 1; // 0x1
    field @FlaggedApi("android.hardware.biometrics.add_fallback") public static final int SETTING = 4; // 0x4
  }
  public static class BiometricManager.Strings {
    method @Nullable @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public CharSequence getButtonLabel();
    method @Nullable @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public CharSequence getPromptMessage();
@@ -19437,9 +19445,11 @@ package android.hardware.biometrics {
    method @Nullable public int getAllowedAuthenticators();
    method @Nullable public android.hardware.biometrics.PromptContentView getContentView();
    method @Nullable public CharSequence getDescription();
    method @FlaggedApi("android.hardware.biometrics.add_fallback") @NonNull public java.util.List<android.hardware.biometrics.FallbackOption> getFallbackOptions();
    method @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap getLogoBitmap();
    method @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String getLogoDescription();
    method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes();
    method @FlaggedApi("android.hardware.biometrics.add_fallback") public static int getMaxFallbackOptions();
    method @Nullable public CharSequence getNegativeButtonText();
    method @Nullable public CharSequence getSubtitle();
    method @NonNull public CharSequence getTitle();
@@ -19485,6 +19495,7 @@ package android.hardware.biometrics {
  public static class BiometricPrompt.Builder {
    ctor public BiometricPrompt.Builder(android.content.Context);
    method @FlaggedApi("android.hardware.biometrics.add_fallback") @NonNull public android.hardware.biometrics.BiometricPrompt.Builder addFallbackOption(@NonNull CharSequence, int, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener);
    method @NonNull public android.hardware.biometrics.BiometricPrompt build();
    method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setAllowedAuthenticators(int);
    method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setConfirmationRequired(boolean);
@@ -19516,6 +19527,15 @@ package android.hardware.biometrics {
    method @Nullable public java.security.Signature getSignature();
  }
  @FlaggedApi("android.hardware.biometrics.add_fallback") public final class FallbackOption implements android.os.Parcelable {
    ctor public FallbackOption(@NonNull CharSequence, int);
    method public int describeContents();
    method public int getIconType();
    method @NonNull public CharSequence getText();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.FallbackOption> CREATOR;
  }
  public interface PromptContentItem {
  }
+47 −1
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.Manifest.permission.TEST_BIOMETRIC;
import static android.Manifest.permission.USE_BIOMETRIC;
import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
import static android.hardware.biometrics.Flags.FLAG_ADD_FALLBACK;

import static com.android.internal.util.FrameworkStatsLog.AUTH_DEPRECATED_APIUSED__DEPRECATED_API__API_BIOMETRIC_MANAGER_CAN_AUTHENTICATE;

@@ -176,6 +177,52 @@ public class BiometricManager {
    @interface BiometricModality {
    }

    /**
     * An {@link IntDef} representing the icons for biometric prompt fallbacks
     */
    @FlaggedApi(FLAG_ADD_FALLBACK)
    public interface IconType {
        /**
         * @hide
         */
        @IntDef({PASSWORD,
                QR_CODE,
                ACCOUNT,
                GENERIC,
                SETTING})
        @Retention(RetentionPolicy.SOURCE)
        @interface Types {}

        /**
         * Password icon
         */
        @FlaggedApi(FLAG_ADD_FALLBACK)
        int PASSWORD = 0;

        /**
         * QR code icon
         */
        @FlaggedApi(FLAG_ADD_FALLBACK)
        int QR_CODE = 1;

        /**
         * Account icon
         */
        @FlaggedApi(FLAG_ADD_FALLBACK)
        int ACCOUNT = 2;

        /**
         * Generic icon
         */
        @FlaggedApi(FLAG_ADD_FALLBACK)
        int GENERIC = 3;

        /**
         * Gear icon
         */
        @FlaggedApi(FLAG_ADD_FALLBACK)
        int SETTING = 4;
    }

    /**
     * Types of authenticators, defined at a level of granularity supported by
@@ -884,6 +931,5 @@ public class BiometricManager {
        }
        return map;
    }

}
+165 −35
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import static android.Manifest.permission.USE_BIOMETRIC;
import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
import static android.hardware.biometrics.BiometricManager.Authenticators;
import static android.hardware.biometrics.Flags.FLAG_ADD_KEY_AGREEMENT_CRYPTO_OBJECT;
import static android.hardware.biometrics.Flags.FLAG_ADD_FALLBACK;
import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT;
import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;

@@ -74,6 +75,10 @@ import javax.crypto.Mac;
public class BiometricPrompt implements BiometricAuthenticator, BiometricConstants {

    private static final String TAG = "BiometricPrompt";

    @VisibleForTesting
    static final int MAX_FALLBACK_OPTIONS = 4;

    @VisibleForTesting
    static final int MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER = 30;

@@ -144,6 +149,22 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
     */
    public static final int DISMISSED_REASON_ERROR_NO_WM = 9;

    /**
     * Dialog is done animating away after user clicked on a fallback option
     * @hide
     */
    public static final int DISMISSED_REASON_FALLBACK_OPTION_BASE = 20;

    // Reserve 20-29 for potential expanded fallback options

    /**
     * Maximum fallback option dismissed value
     * @hide
    */
    public static final int DISMISSED_REASON_FALLBACK_OPTION_MAX =
            DISMISSED_REASON_FALLBACK_OPTION_BASE + MAX_FALLBACK_OPTIONS;


    /**
     * @hide
     */
@@ -155,7 +176,9 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
            DISMISSED_REASON_SERVER_REQUESTED,
            DISMISSED_REASON_CREDENTIAL_CONFIRMED,
            DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS,
            DISMISSED_REASON_ERROR_NO_WM})
            DISMISSED_REASON_ERROR_NO_WM,
            DISMISSED_REASON_FALLBACK_OPTION_BASE,
            DISMISSED_REASON_FALLBACK_OPTION_MAX})
    @Retention(RetentionPolicy.SOURCE)
    public @interface DismissedReason {}

@@ -172,10 +195,12 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
     * A builder that collects arguments to be shown on the system-provided biometric dialog.
     */
    public static class Builder {
        private PromptInfo mPromptInfo;
        private final PromptInfo mPromptInfo;
        private ButtonInfo mNegativeButtonInfo;
        private ButtonInfo mContentViewMoreOptionsButtonInfo;
        private Context mContext;
        private final ButtonInfo[] mFallbackOptions = new ButtonInfo[MAX_FALLBACK_OPTIONS];
        private int mFallbackOptionCount = 0;
        private final Context mContext;
        private IAuthService mService;

        // LINT.IfChange
@@ -387,13 +412,24 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
        }

        /**
         * Required: Sets the text, executor, and click listener for the negative button on the
         * Optional: Sets the text, executor, and click listener for the negative button on the
         * prompt. This is typically a cancel button, but may be also used to show an alternative
         * method for authentication, such as a screen that asks for a backup password.
         *
         * <p>Note that this setting is not required, and in fact is explicitly disallowed, if
         * device credential authentication is enabled via {@link #setAllowedAuthenticators(int)} or
         * {@link #setDeviceCredentialAllowed(boolean)}.
         * <p> If not provided and no fallback is added through
         * {@link #addFallbackOption(CharSequence, int, Executor, DialogInterface.OnClickListener)}
         * (API 36.1 and later),
         * the option to use device credential will be
         * shown as the negative button if allowed in {@link #setAllowedAuthenticators(int)}. If
         * credential is not allowed, "Cancel" will be shown as the negative button.
         *
         * <p> In API 36 and earlier, this setting is required. Note that this setting is not
         * required, and in fact is explicitly disallowed, if
         * device credential authentication is enabled via {@link #setAllowedAuthenticators(int)}
         * or
         * {@link #setDeviceCredentialAllowed(boolean)}. To use credential authentication and
         * provide custom behavior, use
         * {@link #addFallbackOption(CharSequence, int, Executor, DialogInterface.OnClickListener)}
         *
         * @param text     Text to be shown on the negative button for the prompt.
         * @param executor Executor that will be used to run the on click callback.
@@ -418,6 +454,73 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
            return this;
        }

        /**
         * Optional: Sets the text, icon, executor, and click listener for a fallback option in
         * biometric prompt.
         *
         * <p>Fallback option is displayed as the negative button in prompt
         * authentication screen if it is the only option and will display in a list in the
         * fallback
         * options page if there are more than one. If device credential authentication is allowed
         * through {@link #setAllowedAuthenticators(int)}, it will
         * count as a fallback option.
         *
         * <p> If a fallback option is not provided and
         * {@link #setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)} is
         * not set, the option to use device credential will be
         * shown as the negative button if allowed in {@link #setAllowedAuthenticators(int)}. If
         * credential is not allowed, "Cancel" will be shown as the negative button.
         *
         * <p>A maximum of {@link #getMaxFallbackOptions()} fallback options can be added. This
         * number does not include device credential authentication.
         *
         * <p><b>Note: These fallback options will not be displayed if
         * {@link #setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)} is
         * set</b>
         *
         * @param text     Text to be shown on the fallback option for the prompt.
         * @param iconType Icon to be shown for the fallback option
         * @param executor Executor that will be used to run the on click callback.
         * @param listener Listener containing a callback to be run when the button is pressed.
         * @return This builder.
         */
        @NonNull
        @FlaggedApi(FLAG_ADD_FALLBACK)
        public Builder addFallbackOption(@NonNull CharSequence text,
                @BiometricManager.IconType.Types int iconType,
                @NonNull @CallbackExecutor Executor executor,
                @NonNull DialogInterface.OnClickListener listener) {
            if (TextUtils.isEmpty(text)) {
                throw new IllegalArgumentException("Text must be set and non-empty");
            }
            if (executor == null) {
                throw new IllegalArgumentException("Executor must not be null");
            }
            if (listener == null) {
                throw new IllegalArgumentException("Listener must not be null");
            }
            if (!isValidIconType(iconType)) {
                throw new IllegalArgumentException("Invalid icon type");
            }
            if (mFallbackOptionCount >= getMaxFallbackOptions()) {
                throw new IllegalArgumentException(
                        "Maximum fallback option count of " + MAX_FALLBACK_OPTIONS + " exceeded");
            }
            mPromptInfo.addFallbackOption(new FallbackOption(text, iconType));
            mFallbackOptions[mFallbackOptionCount] = new ButtonInfo(executor, listener);
            mFallbackOptionCount++;
            return this;
        }

        @FlaggedApi(FLAG_ADD_FALLBACK)
        private static boolean isValidIconType(@BiometricManager.IconType.Types int iconType) {
            return iconType == BiometricManager.IconType.PASSWORD
                    || iconType == BiometricManager.IconType.QR_CODE
                    || iconType == BiometricManager.IconType.ACCOUNT
                    || iconType == BiometricManager.IconType.GENERIC
                    || iconType == BiometricManager.IconType.SETTING;
        }

        /**
         * Optional: Sets a hint to the system for whether to require user confirmation after
         * authentication. For example, implicit modalities like face and iris are passive, meaning
@@ -668,25 +771,15 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
        @NonNull
        public BiometricPrompt build() {
            final CharSequence title = mPromptInfo.getTitle();
            final CharSequence negative = mPromptInfo.getNegativeButtonText();
            final boolean useDefaultTitle = mPromptInfo.isUseDefaultTitle();
            final boolean deviceCredentialAllowed = mPromptInfo.isDeviceCredentialAllowed();
            final @Authenticators.Types int authenticators = mPromptInfo.getAuthenticators();
            final boolean willShowDeviceCredentialButton = deviceCredentialAllowed
                    || isCredentialAllowed(authenticators);

            if (TextUtils.isEmpty(title) && !useDefaultTitle) {
                throw new IllegalArgumentException("Title must be set and non-empty");
            } else if (TextUtils.isEmpty(negative) && !willShowDeviceCredentialButton) {
                throw new IllegalArgumentException("Negative text must be set and non-empty");
            } else if (!TextUtils.isEmpty(negative) && willShowDeviceCredentialButton) {
                throw new IllegalArgumentException("Can't have both negative button behavior"
                        + " and device credential enabled");
            }
            mService = (mService == null) ? IAuthService.Stub.asInterface(
                    ServiceManager.getService(Context.AUTH_SERVICE)) : mService;
            return new BiometricPrompt(mContext, mPromptInfo, mNegativeButtonInfo,
                    mContentViewMoreOptionsButtonInfo, mService);
                    mContentViewMoreOptionsButtonInfo, mFallbackOptions, mService);
        }
    }

@@ -713,6 +806,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
    private final IAuthService mService;
    private final PromptInfo mPromptInfo;
    private final ButtonInfo mNegativeButtonInfo;
    private final ButtonInfo[] mFallbackOptions;
    private final ButtonInfo mContentViewMoreOptionsButtonInfo;

    private CryptoObject mCryptoObject;
@@ -797,7 +891,20 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
        @Override
        public void onDialogDismissed(int reason) {
            // Check the reason and invoke OnClickListener(s) if necessary
            if (reason == DISMISSED_REASON_NEGATIVE) {
            if (reason >= DISMISSED_REASON_FALLBACK_OPTION_BASE
                    && reason < DISMISSED_REASON_FALLBACK_OPTION_MAX) {
                int buttonIndex = reason - DISMISSED_REASON_FALLBACK_OPTION_BASE;
                if (mFallbackOptions[buttonIndex] != null) {
                    mFallbackOptions[buttonIndex].executor.execute(() -> {
                        mFallbackOptions[buttonIndex].listener.onClick(null,
                                DialogInterface.BUTTON_NEGATIVE);
                        mIsPromptShowing = false;
                    });
                } else {
                    mAuthenticationCallback.onAuthenticationError(BIOMETRIC_ERROR_USER_CANCELED,
                            null /* errString */);
                }
            } else if (reason == DISMISSED_REASON_NEGATIVE) {
                if (mNegativeButtonInfo != null) {
                    mNegativeButtonInfo.executor.execute(() -> {
                        mNegativeButtonInfo.listener.onClick(null, DialogInterface.BUTTON_NEGATIVE);
@@ -831,10 +938,12 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
    private boolean mIsPromptShowing;

    private BiometricPrompt(Context context, PromptInfo promptInfo, ButtonInfo negativeButtonInfo,
            ButtonInfo contentViewMoreOptionsButtonInfo, IAuthService service) {
            ButtonInfo contentViewMoreOptionsButtonInfo, ButtonInfo[] fallbackOptions,
            IAuthService service) {
        mContext = context;
        mPromptInfo = promptInfo;
        mNegativeButtonInfo = negativeButtonInfo;
        mFallbackOptions = fallbackOptions;
        mContentViewMoreOptionsButtonInfo = contentViewMoreOptionsButtonInfo;
        mService = service;
        mIsPromptShowing = false;
@@ -946,6 +1055,17 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
        return mPromptInfo.getNegativeButtonText();
    }

    /**
     * Gets the fallback options for the prompt, as set by
     * {@link Builder#addFallbackOption(CharSequence, int, Executor, DialogInterface.OnClickListener)}.
     * @return The fallback options for the prompt.
     */
    @NonNull
    @FlaggedApi(FLAG_ADD_FALLBACK)
    public List<FallbackOption> getFallbackOptions() {
        return mPromptInfo.getFallbackOptions();
    }

    /**
     * Determines if explicit user confirmation is required by the prompt, as set by
     * {@link Builder#setConfirmationRequired(boolean)}.
@@ -1330,7 +1450,8 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
     * operation can be canceled by using the provided cancel object. The application will receive
     * authentication errors through {@link AuthenticationCallback}, and button events through the
     * corresponding callback set in {@link Builder#setNegativeButton(CharSequence, Executor,
     * DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricPrompt} object,
     * DialogInterface.OnClickListener)} or
     * {@link Builder#addFallbackOption(CharSequence, int, Executor, DialogInterface.OnClickListener)}. It is safe to reuse the {@link BiometricPrompt} object,
     * and calling {@link BiometricPrompt#authenticate(CancellationSignal, Executor,
     * AuthenticationCallback)} while an existing authentication attempt is occurring will stop the
     * previous client and start a new authentication. The interrupted client will receive a
@@ -1406,24 +1527,25 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
     * the user dismisses the system-provided dialog.  This operation can be canceled by using the
     * provided cancel object. The application will receive authentication errors through {@link
     * AuthenticationCallback}, and button events through the corresponding callback set in {@link
     * Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}.  It is
     * safe to reuse the {@link BiometricPrompt} object, and calling {@link
     * Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)} or
     * {@link Builder#addFallbackOption(CharSequence, int, Executor,
     * DialogInterface.OnClickListener)}.
     * It is safe to reuse the {@link BiometricPrompt} object, and calling {@link
     * BiometricPrompt#authenticate(CancellationSignal, Executor, AuthenticationCallback)} while
     * an existing authentication attempt is occurring will stop the previous client and start a new
     * authentication. The interrupted client will receive a cancelled notification through {@link
     * AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
     * an existing authentication attempt is occurring will stop the previous client and start a
     * new authentication. The interrupted client will receive a cancelled notification through
     * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
     *
     * <p>Note: Applications generally should not cancel and start authentication in quick
     * succession. For example, to properly handle authentication across configuration changes, it's
     * recommended to use BiometricPrompt in a fragment with setRetainInstance(true). By doing so,
     * the application will not need to cancel/restart authentication during the configuration
     * succession. For example, to properly handle authentication across configuration changes,
     * it's recommended to use BiometricPrompt in a fragment with setRetainInstance(true). By doing
     * so, the application will not need to cancel/restart authentication during the configuration
     * change.
     *
     * @throws IllegalArgumentException If any of the arguments are null.
     *
     * @param cancel   An object that can be used to cancel authentication.
     * @param executor An executor to handle callback events.
     * @param callback An object to receive authentication events.
     * @throws IllegalArgumentException If any of the arguments are null.
     */
    @RequiresPermission(USE_BIOMETRIC)
    public void authenticate(@NonNull CancellationSignal cancel,
@@ -1532,6 +1654,14 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan
        }
    }

    /**
     * @return the maximum amount of fallback options that can be added to the prompt
     */
    @FlaggedApi(FLAG_ADD_FALLBACK)
    public static int getMaxFallbackOptions() {
        return MAX_FALLBACK_OPTIONS;
    }

    private static boolean isCredentialAllowed(@Authenticators.Types int allowedAuthenticators) {
        return (allowedAuthenticators & Authenticators.DEVICE_CREDENTIAL) != 0;
    }
+77 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 android.hardware.biometrics;

import static android.hardware.biometrics.Flags.FLAG_ADD_FALLBACK;

import android.annotation.FlaggedApi;
import android.os.Parcel;
import android.os.Parcelable;

import androidx.annotation.NonNull;

/**
 * Contains the information for a fallback option to be displayed within Biometric Prompt.
 */
@FlaggedApi(FLAG_ADD_FALLBACK)
public final class FallbackOption implements Parcelable {
    @NonNull private final CharSequence mText;
    @BiometricManager.IconType.Types private final int mIconType;

    public @NonNull CharSequence getText() {
        return mText;
    }

    public @BiometricManager.IconType.Types int getIconType() {
        return mIconType;
    }

    public FallbackOption(@NonNull CharSequence text,
            @BiometricManager.IconType.Types int iconType) {
        this.mText = text;
        this.mIconType = iconType;
    }

    private FallbackOption(Parcel in) {
        mText = in.readCharSequence();
        mIconType = in.readInt();
    }

    @NonNull public static final Creator<FallbackOption> CREATOR =
            new Creator<>() {
                @Override
                public FallbackOption createFromParcel(Parcel in) {
                    return new FallbackOption(in);
                }

                @Override
                public FallbackOption[] newArray(int size) {
                    return new FallbackOption[size];
                }
            };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeCharSequence(mText);
        dest.writeInt(mIconType);
    }
}
+23 −1

File changed.

Preview size limit exceeded, changes collapsed.

Loading