Merge "Add a templated customized view API on Biometric Prompt." into main
diff --git a/core/api/current.txt b/core/api/current.txt
index cbbd94e..46c8f82 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -18680,6 +18680,7 @@
method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public void authenticate(@NonNull android.hardware.biometrics.BiometricPrompt.CryptoObject, @NonNull android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback);
method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public void authenticate(@NonNull android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback);
method @Nullable public int getAllowedAuthenticators();
+ method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView();
method @Nullable public CharSequence getDescription();
method @Nullable public CharSequence getNegativeButtonText();
method @Nullable public CharSequence getSubtitle();
@@ -18727,6 +18728,7 @@
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);
+ method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setContentView(@NonNull android.hardware.biometrics.PromptContentView);
method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence);
method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean);
method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener);
@@ -18750,6 +18752,43 @@
method @Nullable public java.security.Signature getSignature();
}
+ @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentListItem {
+ }
+
+ @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem {
+ ctor public PromptContentListItemBulletedText(@NonNull CharSequence);
+ method public int describeContents();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentListItemBulletedText> CREATOR;
+ }
+
+ @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem {
+ ctor public PromptContentListItemPlainText(@NonNull CharSequence);
+ method public int describeContents();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentListItemPlainText> CREATOR;
+ }
+
+ @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentView {
+ }
+
+ @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView {
+ method public int describeContents();
+ method @Nullable public CharSequence getDescription();
+ method @NonNull public java.util.List<android.hardware.biometrics.PromptContentListItem> getListItems();
+ method public static int getMaxEachItemCharacterNumber();
+ method public static int getMaxItemCount();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptVerticalListContentView> CREATOR;
+ }
+
+ public static final class PromptVerticalListContentView.Builder {
+ ctor public PromptVerticalListContentView.Builder();
+ method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentListItem);
+ method @NonNull public android.hardware.biometrics.PromptVerticalListContentView build();
+ method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull CharSequence);
+ }
+
}
package android.hardware.camera2 {
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 8c1ea5f..a0f4d8d 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -22,6 +22,7 @@
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_GET_OP_ID_CRYPTO_OBJECT;
+import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
@@ -208,6 +209,12 @@
/**
* Optional: Sets a description that will be shown on the prompt.
+ *
+ * <p> Note that the description set by {@link Builder#setDescription(CharSequence)} will be
+ * overridden by {@link Builder#setContentView(PromptContentView)}. The view provided to
+ * {@link Builder#setContentView(PromptContentView)} will be used if both methods are
+ * called.
+ *
* @param description The description to display.
* @return This builder.
*/
@@ -218,6 +225,24 @@
}
/**
+ * Optional: Sets application customized content view that will be shown on the prompt.
+ *
+ * <p> Note that the description set by {@link Builder#setDescription(CharSequence)} will be
+ * overridden by {@link Builder#setContentView(PromptContentView)}. The view provided to
+ * {@link Builder#setContentView(PromptContentView)} will be used if both methods are
+ * called.
+ *
+ * @param view The customized view information.
+ * @return This builder.re
+ */
+ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+ @NonNull
+ public BiometricPrompt.Builder setContentView(@NonNull PromptContentView view) {
+ mPromptInfo.setContentView(view);
+ return this;
+ }
+
+ /**
* @param service
* @return This builder.
* @hide
@@ -698,6 +723,18 @@
}
/**
+ * Gets the content view for the prompt, as set by
+ * {@link Builder#setContentView(PromptContentView)}.
+ *
+ * @return The content view for the prompt, or null if the prompt has no content view.
+ */
+ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+ @Nullable
+ public PromptContentView getContentView() {
+ return mPromptInfo.getContentView();
+ }
+
+ /**
* Gets the negative button text for the prompt, as set by
* {@link Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}.
* @return The negative button text for the prompt, or null if no negative button text was set.
diff --git a/core/java/android/hardware/biometrics/PromptContentListItem.java b/core/java/android/hardware/biometrics/PromptContentListItem.java
new file mode 100644
index 0000000..fa3783d
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItem.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 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_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+
+/**
+ * A list item shown on {@link PromptVerticalListContentView}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public interface PromptContentListItem {
+}
+
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java
new file mode 100644
index 0000000..c31f8a6
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public final class PromptContentListItemBulletedText implements PromptContentListItemParcelable {
+ private final CharSequence mText;
+
+ /**
+ * A list item with bulleted text shown on {@link PromptVerticalListContentView}.
+ *
+ * @param text The text of this list item.
+ */
+ public PromptContentListItemBulletedText(@NonNull CharSequence text) {
+ mText = text;
+ }
+
+ /**
+ * @hide
+ */
+ @NonNull
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeCharSequence(mText);
+ }
+
+ /**
+ * @see Parcelable.Creator
+ */
+ @NonNull
+ public static final Creator<PromptContentListItemBulletedText> CREATOR = new Creator<>() {
+ @Override
+ public PromptContentListItemBulletedText createFromParcel(Parcel in) {
+ return new PromptContentListItemBulletedText(in.readCharSequence());
+ }
+
+ @Override
+ public PromptContentListItemBulletedText[] newArray(int size) {
+ return new PromptContentListItemBulletedText[size];
+ }
+ };
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java b/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java
new file mode 100644
index 0000000..15271f0
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcelable;
+
+/**
+ * A parcelable {@link PromptContentListItem}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+sealed interface PromptContentListItemParcelable extends PromptContentListItem, Parcelable
+ permits PromptContentListItemPlainText, PromptContentListItemBulletedText {
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java
new file mode 100644
index 0000000..d72a758
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A list item with plain text shown on {@link PromptVerticalListContentView}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public final class PromptContentListItemPlainText implements PromptContentListItemParcelable {
+ private final CharSequence mText;
+
+ /**
+ * A list item with plain text shown on {@link PromptVerticalListContentView}.
+ *
+ * @param text The text of this list item.
+ */
+ public PromptContentListItemPlainText(@NonNull CharSequence text) {
+ mText = text;
+ }
+
+ /**
+ * @hide
+ */
+ @NonNull
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeCharSequence(mText);
+ }
+
+ /**
+ * @see Parcelable.Creator
+ */
+ @NonNull
+ public static final Creator<PromptContentListItemPlainText> CREATOR = new Creator<>() {
+ @Override
+ public PromptContentListItemPlainText createFromParcel(Parcel in) {
+ return new PromptContentListItemPlainText(in.readCharSequence());
+ }
+
+ @Override
+ public PromptContentListItemPlainText[] newArray(int size) {
+ return new PromptContentListItemPlainText[size];
+ }
+ };
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentView.java b/core/java/android/hardware/biometrics/PromptContentView.java
new file mode 100644
index 0000000..ff9313e
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentView.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+
+/**
+ * Contains the information of the template of content view for Biometric Prompt.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public interface PromptContentView {
+}
diff --git a/core/java/android/hardware/biometrics/PromptContentViewParcelable.java b/core/java/android/hardware/biometrics/PromptContentViewParcelable.java
new file mode 100644
index 0000000..43b965b
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptContentViewParcelable.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.os.Parcelable;
+
+/**
+ * A parcelable {@link PromptContentView}.
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+sealed interface PromptContentViewParcelable extends PromptContentView, Parcelable
+ permits PromptVerticalListContentView {
+}
diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java
index 24cfd164..c73ebd4 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -35,6 +35,7 @@
@Nullable private CharSequence mSubtitle;
private boolean mUseDefaultSubtitle;
@Nullable private CharSequence mDescription;
+ @Nullable private PromptContentViewParcelable mContentView;
@Nullable private CharSequence mDeviceCredentialTitle;
@Nullable private CharSequence mDeviceCredentialSubtitle;
@Nullable private CharSequence mDeviceCredentialDescription;
@@ -60,6 +61,8 @@
mSubtitle = in.readCharSequence();
mUseDefaultSubtitle = in.readBoolean();
mDescription = in.readCharSequence();
+ mContentView = in.readParcelable(PromptContentViewParcelable.class.getClassLoader(),
+ PromptContentViewParcelable.class);
mDeviceCredentialTitle = in.readCharSequence();
mDeviceCredentialSubtitle = in.readCharSequence();
mDeviceCredentialDescription = in.readCharSequence();
@@ -100,6 +103,7 @@
dest.writeCharSequence(mSubtitle);
dest.writeBoolean(mUseDefaultSubtitle);
dest.writeCharSequence(mDescription);
+ dest.writeParcelable(mContentView, 0);
dest.writeCharSequence(mDeviceCredentialTitle);
dest.writeCharSequence(mDeviceCredentialSubtitle);
dest.writeCharSequence(mDeviceCredentialDescription);
@@ -176,6 +180,10 @@
mDescription = description;
}
+ public void setContentView(PromptContentView view) {
+ mContentView = (PromptContentViewParcelable) view;
+ }
+
public void setDeviceCredentialTitle(CharSequence deviceCredentialTitle) {
mDeviceCredentialTitle = deviceCredentialTitle;
}
@@ -257,6 +265,15 @@
return mDescription;
}
+ /**
+ * Gets the content view for the prompt.
+ *
+ * @return The content view for the prompt, or null if the prompt has no content view.
+ */
+ public PromptContentView getContentView() {
+ return mContentView;
+ }
+
public CharSequence getDeviceCredentialTitle() {
return mDeviceCredentialTitle;
}
diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
new file mode 100644
index 0000000..f3cb189
--- /dev/null
+++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2023 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_CUSTOM_BIOMETRIC_PROMPT;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Contains the information of the template of vertical list content view for Biometric Prompt. Note
+ * that there are limits on the item count and the number of characters allowed for each item's
+ * text.
+ * <p>
+ * Here's how you'd set a <code>PromptVerticalListContentView</code> on a Biometric Prompt:
+ * <pre class="prettyprint">
+ * BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(...)
+ * .setTitle(...)
+ * .setSubTitle(...)
+ * .setContentView(new PromptVerticalListContentView.Builder()
+ * .setDescription("test description")
+ * .addListItem(new PromptContentListItemPlainText("test item 1"))
+ * .addListItem(new PromptContentListItemPlainText("test item 2"))
+ * .addListItem(new PromptContentListItemBulletedText("test item 3"))
+ * .build())
+ * .build();
+ * </pre>
+ */
+@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+public final class PromptVerticalListContentView implements PromptContentViewParcelable {
+ private static final int MAX_ITEM_NUMBER = 20;
+ private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;
+ private final List<PromptContentListItemParcelable> mContentList;
+ private final CharSequence mDescription;
+
+ private PromptVerticalListContentView(
+ @NonNull List<PromptContentListItemParcelable> contentList,
+ @NonNull CharSequence description) {
+ mContentList = contentList;
+ mDescription = description;
+ }
+
+ private PromptVerticalListContentView(Parcel in) {
+ mContentList = in.readArrayList(
+ PromptContentListItemParcelable.class.getClassLoader(),
+ PromptContentListItemParcelable.class);
+ mDescription = in.readCharSequence();
+ }
+
+ /**
+ * Returns the maximum count of the list items.
+ */
+ public static int getMaxItemCount() {
+ return MAX_ITEM_NUMBER;
+ }
+
+ /**
+ * Returns the maximum number of characters allowed for each item's text.
+ */
+ public static int getMaxEachItemCharacterNumber() {
+ return MAX_EACH_ITEM_CHARACTER_NUMBER;
+ }
+
+ /**
+ * Gets the description for the content view, as set by
+ * {@link PromptVerticalListContentView.Builder#setDescription(CharSequence)}.
+ *
+ * @return The description for the content view, or null if the content view has no description.
+ */
+ @Nullable
+ public CharSequence getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Gets the list of ListItem on the content view, as set by
+ * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentListItem)}.
+ *
+ * @return The item list on the content view.
+ */
+ @NonNull
+ public List<PromptContentListItem> getListItems() {
+ return new ArrayList<>(mContentList);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) {
+ dest.writeList(mContentList);
+ dest.writeCharSequence(mDescription);
+ }
+
+ /**
+ * @see Parcelable.Creator
+ */
+ @NonNull
+ public static final Creator<PromptVerticalListContentView> CREATOR = new Creator<>() {
+ @Override
+ public PromptVerticalListContentView createFromParcel(Parcel in) {
+ return new PromptVerticalListContentView(in);
+ }
+
+ @Override
+ public PromptVerticalListContentView[] newArray(int size) {
+ return new PromptVerticalListContentView[size];
+ }
+ };
+
+
+ /**
+ * A builder that collects arguments to be shown on the vertical list view.
+ */
+ public static final class Builder {
+ private final List<PromptContentListItemParcelable> mContentList = new ArrayList<>();
+ private CharSequence mDescription;
+
+ /**
+ * Optional: Sets a description that will be shown on the content view.
+ *
+ * @param description The description to display.
+ * @return This builder.
+ */
+ @NonNull
+ public Builder setDescription(@NonNull CharSequence description) {
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Optional: Adds a list item in the current row. Maximum {@value MAX_ITEM_NUMBER} items in
+ * total.
+ *
+ * @param listItem The list item view to display
+ * @return This builder.
+ */
+ @NonNull
+ public Builder addListItem(@NonNull PromptContentListItem listItem) {
+ if (doesListItemExceedsCharLimit(listItem)) {
+ throw new IllegalStateException(
+ "The character number of list item exceeds "
+ + MAX_EACH_ITEM_CHARACTER_NUMBER);
+ }
+ mContentList.add((PromptContentListItemParcelable) listItem);
+ return this;
+ }
+
+ private boolean doesListItemExceedsCharLimit(PromptContentListItem listItem) {
+ if (listItem instanceof PromptContentListItemPlainText) {
+ return ((PromptContentListItemPlainText) listItem).getText().length()
+ > MAX_EACH_ITEM_CHARACTER_NUMBER;
+ } else if (listItem instanceof PromptContentListItemBulletedText) {
+ return ((PromptContentListItemBulletedText) listItem).getText().length()
+ > MAX_EACH_ITEM_CHARACTER_NUMBER;
+ } else {
+ return false;
+ }
+ }
+
+
+ /**
+ * Creates a {@link PromptVerticalListContentView}.
+ *
+ * @return An instance of {@link PromptVerticalListContentView}.
+ */
+ @NonNull
+ public PromptVerticalListContentView build() {
+ if (mContentList.size() > MAX_ITEM_NUMBER) {
+ throw new IllegalStateException(
+ "The number of list items exceeds " + MAX_ITEM_NUMBER);
+ }
+ return new PromptVerticalListContentView(mContentList, mDescription);
+ }
+ }
+}
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 375fdb5..3ba8be4 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -21,3 +21,10 @@
bug: "307601768"
}
+flag {
+ name: "custom_biometric_prompt"
+ namespace: "biometrics_framework"
+ description: "Feature flag for adding a custom content view API to BiometricPrompt.Builder."
+ bug: "302735104"
+}
+
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 15633d1..4a39799 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -18,6 +18,7 @@
import android.hardware.biometrics.BiometricManager.Authenticators
import android.hardware.biometrics.ComponentInfoInternal
+import android.hardware.biometrics.PromptContentView
import android.hardware.biometrics.PromptInfo
import android.hardware.biometrics.SensorProperties
import android.hardware.biometrics.SensorPropertiesInternal
@@ -119,6 +120,7 @@
title: String = "title",
subtitle: String = "sub",
description: String = "desc",
+ contentView: PromptContentView? = null,
credentialTitle: String? = "cred title",
credentialSubtitle: String? = "cred sub",
credentialDescription: String? = "cred desc",
@@ -128,6 +130,7 @@
info.title = title
info.subtitle = subtitle
info.description = description
+ info.contentView = contentView
credentialTitle?.let { info.deviceCredentialTitle = it }
credentialSubtitle?.let { info.deviceCredentialSubtitle = it }
credentialDescription?.let { info.deviceCredentialDescription = it }
diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml
new file mode 100644
index 0000000..3908757
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2023 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/customized_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ style="@style/AuthCredentialContentLayoutStyle">
+
+ <TextView
+ android:id="@+id/customized_view_title"
+ style="@style/TextAppearance.AuthCredential.ContentViewTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="1"
+ android:singleLine="true" />
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml b/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml
new file mode 100644
index 0000000..e39f60f
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/TextAppearance.AuthCredential.ContentViewListItem"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1.0" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml
new file mode 100644
index 0000000..6c86736
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/biometric_prompt_content_list_row_height"
+ android:gravity="center_vertical|start"
+ android:orientation="horizontal" />
diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
index bea0e13..23fbb12 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
@@ -48,6 +48,23 @@
android:importantForAccessibility="no"
style="@style/TextAppearance.AuthCredential.Description"/>
+ <Space
+ android:id="@+id/space_above_content"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/biometric_prompt_space_above_content"
+ android:visibility="gone" />
+
+ <ScrollView
+ android:id="@+id/customized_view_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fadeScrollbars="false"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingHorizontal="@dimen/biometric_prompt_content_container_padding_horizontal"
+ android:scrollbars="vertical"
+ android:visibility="gone" />
+
<Space android:id="@+id/space_above_icon"
android:layout_width="match_parent"
android:layout_height="48dp" />
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 5f6a39a..8be1cc7 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -135,6 +135,9 @@
<color name="biometric_dialog_gray">#ff757575</color>
<color name="biometric_dialog_accent">@color/material_dynamic_primary40</color>
<color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 -->
+ <!-- Color for biometric prompt content view -->
+ <color name="biometric_prompt_content_background_color">#8AB4F8</color>
+ <color name="biometric_prompt_content_list_item_bullet_color">#1d873b</color>
<!-- SFPS colors -->
<color name="sfps_chevron_fill">@color/material_dynamic_primary90</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 90d8cdb..798fc06b 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1092,6 +1092,16 @@
<dimen name="biometric_dialog_width">240dp</dimen>
<dimen name="biometric_dialog_height">240dp</dimen>
+ <!-- Dimensions for biometric prompt content view. -->
+ <dimen name="biometric_prompt_space_above_content">48dp</dimen>
+ <dimen name="biometric_prompt_content_container_padding_horizontal">24dp</dimen>
+ <dimen name="biometric_prompt_content_padding_horizontal">10dp</dimen>
+ <dimen name="biometric_prompt_content_list_row_height">24dp</dimen>
+ <dimen name="biometric_prompt_content_list_item_padding_horizontal">10dp</dimen>
+ <dimen name="biometric_prompt_content_list_item_text_size">14sp</dimen>
+ <dimen name="biometric_prompt_content_list_item_bullet_gap_width">10dp</dimen>
+ <dimen name="biometric_prompt_content_list_item_bullet_radius">5dp</dimen>
+
<!-- Biometric Auth Credential values -->
<dimen name="biometric_auth_icon_size">48dp</dimen>
diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml
index c925b26..fad4d4f 100644
--- a/packages/SystemUI/res/values/integers.xml
+++ b/packages/SystemUI/res/values/integers.xml
@@ -16,6 +16,7 @@
-->
<resources>
<integer name="biometric_dialog_text_gravity">8388611</integer> <!-- gravity start -->
+ <integer name="biometric_prompt_content_list_item_max_lines_if_two_column">3</integer>
<integer name="qs_security_footer_maxLines">2</integer>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7d7c050..ab3cacb 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -195,6 +195,24 @@
<item name="android:textSize">14sp</item>
</style>
+ <style name="TextAppearance.AuthCredential.ContentViewTitle">
+ <item name="android:fontFamily">google-sans</item>
+ <item name="android:paddingTop">8dp</item>
+ <item name="android:paddingHorizontal">24dp</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:gravity">start</item>
+ </style>
+
+ <style name="TextAppearance.AuthCredential.ContentViewListItem">
+ <item name="android:fontFamily">google-sans</item>
+ <item name="android:paddingTop">8dp</item>
+ <item name="android:paddingHorizontal">
+ @dimen/biometric_prompt_content_list_item_padding_horizontal
+ </item>
+ <item name="android:textSize">@dimen/biometric_prompt_content_list_item_text_size</item>
+ <item name="android:gravity">start</item>
+ </style>
+
<style name="TextAppearance.AuthCredential.Error">
<item name="android:paddingTop">6dp</item>
<item name="android:paddingHorizontal">24dp</item>
@@ -294,6 +312,11 @@
<item name="android:textSize">16sp</item>
</style>
+ <style name="AuthCredentialContentLayoutStyle">
+ <item name="android:background">@color/biometric_prompt_content_background_color</item>
+ <item name="android:paddingHorizontal">@dimen/biometric_prompt_content_padding_horizontal</item>
+ </style>
+
<style name="DeviceManagementDialogTitle">
<item name="android:gravity">center</item>
<item name="android:textAppearance">@style/TextAppearance.Dialog.Title</item>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
index abbfa01..86802a5b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
@@ -131,23 +131,21 @@
icon.measure(
MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST));
- } else if (child.getId() == R.id.space_above_icon) {
+ } else if (child.getId() == R.id.space_above_icon
+ || child.getId() == R.id.space_above_content
+ || child.getId() == R.id.button_bar) {
child.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(
child.getLayoutParams().height, MeasureSpec.EXACTLY));
- } else if (child.getId() == R.id.button_bar) {
- child.measure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
- MeasureSpec.EXACTLY));
} else if (child.getId() == R.id.space_below_icon) {
// Set the spacer height so the fingerprint icon is on the physical sensor area
final int clampedSpacerHeight = Math.max(mBottomSpacerHeight, 0);
child.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(clampedSpacerHeight, MeasureSpec.EXACTLY));
- } else if (child.getId() == R.id.description) {
+ } else if (child.getId() == R.id.description
+ || child.getId() == R.id.customized_view_container) {
//skip description view and compute later
continue;
} else {
@@ -161,27 +159,28 @@
}
}
- //re-calculate the height of description
+ //re-calculate the height of body content
View description = mView.findViewById(R.id.description);
+ View contentView = mView.findViewById(R.id.customized_view_container);
if (description != null && description.getVisibility() != View.GONE) {
totalHeight += measureDescription(description, displayHeight, width, totalHeight);
+ } else if (contentView != null && contentView.getVisibility() != View.GONE) {
+ totalHeight += measureDescription(contentView, displayHeight, width, totalHeight);
}
return new AuthDialog.LayoutParams(width, totalHeight);
}
- private int measureDescription(View description, int displayHeight, int currWidth,
+ private int measureDescription(View bodyContent, int displayHeight, int currWidth,
int currHeight) {
- //description view should be measured in AuthBiometricFingerprintView#onMeasureInternal
- //so we could getMeasuredHeight in onMeasureInternalPortrait directly.
- int newHeight = description.getMeasuredHeight() + currHeight;
+ int newHeight = bodyContent.getMeasuredHeight() + currHeight;
int limit = (int) (displayHeight * 0.75);
if (newHeight > limit) {
- description.measure(
+ bodyContent.measure(
MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY));
}
- return description.getMeasuredHeight();
+ return bodyContent.getMeasuredHeight();
}
@NonNull
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
index 8fbb250..4377937 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
@@ -1,5 +1,6 @@
package com.android.systemui.biometrics.domain.model
+import android.hardware.biometrics.PromptContentView
import android.hardware.biometrics.PromptInfo
import com.android.systemui.biometrics.shared.model.BiometricModalities
import com.android.systemui.biometrics.shared.model.BiometricUserInfo
@@ -34,6 +35,7 @@
operationInfo = operationInfo,
showEmergencyCallButton = info.isShowEmergencyCallButton
) {
+ val contentView: PromptContentView? = info.contentView
val negativeButtonText: String = info.negativeButtonText?.toString() ?: ""
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
index 0d72b9c..60b454e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
@@ -101,6 +101,7 @@
final View child = getChildAt(i);
if (child.getId() == R.id.space_above_icon
+ || child.getId() == R.id.space_above_content
|| child.getId() == R.id.space_below_icon
|| child.getId() == R.id.button_bar) {
child.measure(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
new file mode 100644
index 0000000..22b02da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2023 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 com.android.systemui.biometrics.ui.binder
+
+import android.content.Context
+import android.content.res.Resources
+import android.content.res.Resources.Theme
+import android.graphics.Paint
+import android.hardware.biometrics.PromptContentListItem
+import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptContentListItemPlainText
+import android.hardware.biometrics.PromptContentView
+import android.hardware.biometrics.PromptVerticalListContentView
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.style.BulletSpan
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.ScrollView
+import android.widget.Space
+import android.widget.TextView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import kotlin.math.ceil
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+/** Sub-binder for [BiometricPromptLayout.customized_view_container]. */
+object BiometricCustomizedViewBinder {
+ fun bind(customizedViewContainer: ScrollView, spaceAbove: Space, viewModel: PromptViewModel) {
+ customizedViewContainer.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ launch {
+ val contentView: PromptContentView? = viewModel.contentView.first()
+
+ if (contentView != null) {
+ val context = customizedViewContainer.context
+ customizedViewContainer.addView(contentView.toView(context))
+ customizedViewContainer.visibility = View.VISIBLE
+ spaceAbove.visibility = View.VISIBLE
+ } else {
+ customizedViewContainer.visibility = View.GONE
+ spaceAbove.visibility = View.GONE
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun PromptContentView.toView(context: Context): View {
+ val resources = context.resources
+ val inflater = LayoutInflater.from(context)
+ when (this) {
+ is PromptVerticalListContentView -> {
+ val contentView =
+ inflater.inflate(R.layout.biometric_prompt_content_layout, null) as LinearLayout
+
+ val descriptionView = contentView.requireViewById<TextView>(R.id.customized_view_title)
+ if (!description.isNullOrEmpty()) {
+ descriptionView.text = description
+ } else {
+ descriptionView.visibility = View.GONE
+ }
+
+ // Show two column by default, once there is an item exceeding max lines, show single
+ // item instead.
+ val showTwoColumn = listItems.all { !it.doesExceedMaxLinesIfTwoColumn(resources) }
+ var currRowView = createNewRowLayout(inflater)
+ for (item in listItems) {
+ val itemView = item.toView(resources, inflater, context.theme)
+ currRowView.addView(itemView)
+
+ if (!showTwoColumn || currRowView.childCount == 2) {
+ contentView.addView(currRowView)
+ currRowView = createNewRowLayout(inflater)
+ }
+ }
+ if (currRowView.childCount > 0) {
+ contentView.addView(currRowView)
+ }
+
+ return contentView
+ }
+ else -> {
+ throw IllegalStateException("No such PromptContentView: $this")
+ }
+ }
+}
+
+private fun createNewRowLayout(inflater: LayoutInflater): LinearLayout {
+ return inflater.inflate(R.layout.biometric_prompt_content_row_layout, null) as LinearLayout
+}
+
+private fun PromptContentListItem.doesExceedMaxLinesIfTwoColumn(
+ resources: Resources,
+): Boolean {
+ val passedInText: CharSequence =
+ when (this) {
+ is PromptContentListItemPlainText -> text
+ is PromptContentListItemBulletedText -> text
+ else -> {
+ throw IllegalStateException("No such ListItem: $this")
+ }
+ }
+
+ when (this) {
+ is PromptContentListItemPlainText,
+ is PromptContentListItemBulletedText -> {
+ val dialogMargin =
+ resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding)
+ val halfDialogWidth =
+ Resources.getSystem().displayMetrics.widthPixels / 2 - dialogMargin
+ val containerPadding =
+ resources.getDimensionPixelSize(
+ R.dimen.biometric_prompt_content_container_padding_horizontal
+ )
+ val contentPadding =
+ resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_padding_horizontal)
+ val listItemPadding = getListItemPadding(resources)
+ val maxWidth = halfDialogWidth - containerPadding - contentPadding - listItemPadding
+
+ val text = "$passedInText"
+ val textSize =
+ resources.getDimensionPixelSize(
+ R.dimen.biometric_prompt_content_list_item_text_size
+ )
+ val paint = Paint()
+ paint.textSize = textSize.toFloat()
+
+ val maxLines =
+ resources.getInteger(
+ R.integer.biometric_prompt_content_list_item_max_lines_if_two_column
+ )
+ val numLines = ceil(paint.measureText(text).toDouble() / maxWidth).toInt()
+ return numLines > maxLines
+ }
+ else -> {
+ throw IllegalStateException("No such ListItem: $this")
+ }
+ }
+}
+
+private fun PromptContentListItem.toView(
+ resources: Resources,
+ inflater: LayoutInflater,
+ theme: Theme,
+): TextView {
+ val textView =
+ inflater.inflate(R.layout.biometric_prompt_content_row_item_text_view, null) as TextView
+ val lp = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f)
+ textView.layoutParams = lp
+
+ when (this) {
+ is PromptContentListItemPlainText -> {
+ textView.text = text
+ }
+ is PromptContentListItemBulletedText -> {
+ val bulletedText = SpannableString(text)
+ val span =
+ BulletSpan(
+ getListItemBulletGapWidth(resources),
+ getListItemBulletColor(resources, theme),
+ getListItemBulletRadius(resources)
+ )
+ bulletedText.setSpan(span, 0 /* start */, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ textView.text = bulletedText
+ }
+ else -> {
+ throw IllegalStateException("No such ListItem: $this")
+ }
+ }
+ return textView
+}
+
+private fun PromptContentListItem.getListItemPadding(resources: Resources): Int {
+ var listItemPadding =
+ resources.getDimensionPixelSize(
+ R.dimen.biometric_prompt_content_list_item_padding_horizontal
+ ) * 2
+ when (this) {
+ is PromptContentListItemPlainText -> {}
+ is PromptContentListItemBulletedText -> {
+ listItemPadding +=
+ getListItemBulletRadius(resources) * 2 + getListItemBulletGapWidth(resources)
+ }
+ else -> {
+ throw IllegalStateException("No such ListItem: $this")
+ }
+ }
+ return listItemPadding
+}
+
+private fun getListItemBulletRadius(resources: Resources): Int =
+ resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_list_item_bullet_radius)
+
+private fun getListItemBulletGapWidth(resources: Resources): Int =
+ resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_list_item_bullet_gap_width)
+
+private fun getListItemBulletColor(resources: Resources, theme: Theme): Int =
+ resources.getColor(R.color.biometric_prompt_content_list_item_bullet_color, theme)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 7b8cb82..04dc7a8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -31,6 +31,7 @@
import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO
import android.view.accessibility.AccessibilityManager
import android.widget.Button
+import android.widget.ScrollView
import android.widget.TextView
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
@@ -94,6 +95,8 @@
val titleView = view.requireViewById<TextView>(R.id.title)
val subtitleView = view.requireViewById<TextView>(R.id.subtitle)
val descriptionView = view.requireViewById<TextView>(R.id.description)
+ val customizedViewContainer =
+ view.requireViewById<ScrollView>(R.id.customized_view_container)
// set selected to enable marquee unless a screen reader is enabled
titleView.isSelected =
@@ -150,8 +153,13 @@
}
titleView.text = viewModel.title.first()
- descriptionView.text = viewModel.description.first()
subtitleView.text = viewModel.subtitle.first()
+ descriptionView.text = viewModel.description.first()
+ BiometricCustomizedViewBinder.bind(
+ customizedViewContainer,
+ view.requireViewById(R.id.space_above_content),
+ viewModel
+ )
// set button listeners
negativeButton.setOnClickListener { legacyCallback.onButtonNegative() }
@@ -178,12 +186,14 @@
titleView,
subtitleView,
descriptionView,
+ customizedViewContainer,
),
viewsToFadeInOnSizeChange =
listOf(
titleView,
subtitleView,
descriptionView,
+ customizedViewContainer,
indicatorMessageView,
negativeButton,
cancelButton,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
index 1a7b6c9..c3bbaed 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -55,7 +55,7 @@
fun bind(
view: BiometricPromptLayout,
viewModel: PromptViewModel,
- viewsToHideWhenSmall: List<TextView>,
+ viewsToHideWhenSmall: List<View>,
viewsToFadeInOnSizeChange: List<View>,
panelViewController: AuthPanelController,
jankListener: BiometricJankListener,
@@ -110,7 +110,7 @@
// prepare for animated size transitions
for (v in viewsToHideWhenSmall) {
- v.showTextOrHide(forceHide = size.isSmall)
+ v.showContentOrHide(forceHide = size.isSmall)
}
if (currentSize == null && size.isSmall) {
iconHolderView.alpha = 0f
@@ -119,6 +119,10 @@
viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
}
+ // TODO(b/302735104): Fix wrong height due to the delay of
+ // PromptContentView. addOnLayoutChangeListener() will cause crash when
+ // showing credential view, since |PromptIconViewModel| won't release the
+ // flow.
// propagate size changes to legacy panel controller and animate transitions
view.doOnLayout {
val width = view.measuredWidth
@@ -228,8 +232,9 @@
return r == Surface.ROTATION_90 || r == Surface.ROTATION_270
}
-private fun TextView.showTextOrHide(forceHide: Boolean = false) {
- visibility = if (forceHide || text.isBlank()) View.GONE else View.VISIBLE
+private fun View.showContentOrHide(forceHide: Boolean = false) {
+ val isTextViewWithBlankText = this is TextView && this.text.isBlank()
+ visibility = if (forceHide || isTextViewWithBlankText) View.GONE else View.VISIBLE
}
private fun View.asVerticalAnimator(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index d899827e..1c78928 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.systemui.biometrics.ui.viewmodel
import android.content.Context
import android.graphics.Rect
import android.hardware.biometrics.BiometricPrompt
+import android.hardware.biometrics.PromptContentView
import android.util.Log
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
@@ -239,9 +241,20 @@
val subtitle: Flow<String> =
promptSelectorInteractor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged()
- /** Description for the prompt. */
- val description: Flow<String> =
+ /** Custom content view for the prompt. */
+ val contentView: Flow<PromptContentView?> =
+ promptSelectorInteractor.prompt.map { it?.contentView }.distinctUntilChanged()
+
+ private val originalDescription =
promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
+ /**
+ * Description for the prompt. Description view and contentView is mutually exclusive. Pass
+ * description down only when contentView is null.
+ */
+ val description: Flow<String> =
+ combine(contentView, originalDescription) { contentView, description ->
+ if (contentView == null) description else ""
+ }
/** If the indicator (help, error) message should be shown. */
val isIndicatorMessageVisible: Flow<Boolean> =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
index 9e3c576..bd4973d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
@@ -1,5 +1,7 @@
package com.android.systemui.biometrics.domain.model
+import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptVerticalListContentView
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
@@ -23,11 +25,21 @@
val title = "what"
val subtitle = "a"
val description = "request"
+ val contentView =
+ PromptVerticalListContentView.Builder()
+ .setDescription("content description")
+ .addListItem(PromptContentListItemBulletedText("content text"))
+ .build()
val fpPros = fingerprintSensorPropertiesInternal().first()
val request =
BiometricPromptRequest.Biometric(
- promptInfo(title = title, subtitle = subtitle, description = description),
+ promptInfo(
+ title = title,
+ subtitle = subtitle,
+ description = description,
+ contentView = contentView
+ ),
BiometricUserInfo(USER_ID),
BiometricOperationInfo(OPERATION_ID),
BiometricModalities(fingerprintProperties = fpPros),
@@ -36,6 +48,7 @@
assertThat(request.title).isEqualTo(title)
assertThat(request.subtitle).isEqualTo(subtitle)
assertThat(request.description).isEqualTo(description)
+ assertThat(request.contentView).isEqualTo(contentView)
assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
assertThat(request.modalities)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 6170e0c..bf61c2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -18,7 +18,10 @@
import android.content.res.Configuration
import android.graphics.Point
+import android.hardware.biometrics.PromptContentListItemBulletedText
+import android.hardware.biometrics.PromptContentView
import android.hardware.biometrics.PromptInfo
+import android.hardware.biometrics.PromptVerticalListContentView
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
@@ -60,7 +63,6 @@
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -69,7 +71,6 @@
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mockito.Mock
-import org.mockito.Mockito.times
import org.mockito.junit.MockitoJUnit
private const val USER_ID = 4
@@ -101,6 +102,7 @@
private lateinit var selector: PromptSelectorInteractor
private lateinit var viewModel: PromptViewModel
private lateinit var iconViewModel: PromptIconViewModel
+ private lateinit var promptContentView: PromptContentView
@Before
fun setup() {
@@ -136,6 +138,10 @@
selector =
PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils)
selector.resetPrompt()
+ promptContentView =
+ PromptVerticalListContentView.Builder()
+ .addListItem(PromptContentListItemBulletedText("test"))
+ .build()
viewModel =
PromptViewModel(
@@ -1200,6 +1206,26 @@
}
}
+ @Test
+ fun descriptionOverriddenByContentView() =
+ runGenericTest(contentView = promptContentView, description = "test description") {
+ val contentView by collectLastValue(viewModel.contentView)
+ val description by collectLastValue(viewModel.description)
+
+ assertThat(description).isEqualTo("")
+ assertThat(contentView).isEqualTo(promptContentView)
+ }
+
+ @Test
+ fun descriptionWithoutContentView() =
+ runGenericTest(description = "test description") {
+ val contentView by collectLastValue(viewModel.contentView)
+ val description by collectLastValue(viewModel.description)
+
+ assertThat(description).isEqualTo("test description")
+ assertThat(contentView).isNull()
+ }
+
/** Asserts that the selected buttons are visible now. */
private suspend fun TestScope.assertButtonsVisible(
tryAgain: Boolean = false,
@@ -1219,6 +1245,8 @@
private fun runGenericTest(
doNotStart: Boolean = false,
allowCredentialFallback: Boolean = false,
+ description: String? = null,
+ contentView: PromptContentView? = null,
block: suspend TestScope.() -> Unit
) {
selector.initializePrompt(
@@ -1226,6 +1254,8 @@
allowCredentialFallback = allowCredentialFallback,
fingerprint = testCase.fingerprint,
face = testCase.face,
+ descriptionFromApp = description,
+ contentViewFromApp = contentView,
)
// put the view model in the initial authenticating state, unless explicitly skipped
@@ -1401,11 +1431,15 @@
face: FaceSensorPropertiesInternal? = null,
requireConfirmation: Boolean = false,
allowCredentialFallback: Boolean = false,
+ descriptionFromApp: String? = null,
+ contentViewFromApp: PromptContentView? = null,
) {
val info =
PromptInfo().apply {
title = "t"
subtitle = "s"
+ description = descriptionFromApp
+ contentView = contentViewFromApp
authenticators = listOf(face, fingerprint).extractAuthenticatorTypes()
isDeviceCredentialAllowed = allowCredentialFallback
isConfirmationRequested = requireConfirmation