Merge "Support small clock for the lock screen preview" into udc-dev
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index df9257c..5c1b3ee 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5006,12 +5006,6 @@
return mUserExtras;
}
- private Bundle getAllExtras() {
- final Bundle saveExtras = (Bundle) mUserExtras.clone();
- saveExtras.putAll(mN.extras);
- return saveExtras;
- }
-
/**
* Add an action to this notification. Actions are typically displayed by
* the system as a button adjacent to the notification content.
@@ -6617,9 +6611,16 @@
+ " vs bubble: " + mN.mBubbleMetadata.getShortcutId());
}
- // first, add any extras from the calling code
+ // Adds any new extras provided by the user.
if (mUserExtras != null) {
- mN.extras = getAllExtras();
+ final Bundle saveExtras = (Bundle) mUserExtras.clone();
+ if (SystemProperties.getBoolean(
+ "persist.sysui.notification.builder_extras_override", false)) {
+ mN.extras.putAll(saveExtras);
+ } else {
+ saveExtras.putAll(mN.extras);
+ mN.extras = saveExtras;
+ }
}
mN.creationTime = System.currentTimeMillis();
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 7f19897..8fafb18 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2883,6 +2883,20 @@
"android.software.car.templates_host";
/**
+ * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:If this
+ * feature is supported, the device should also declare {@link #FEATURE_AUTOMOTIVE} and show
+ * a UI that can display multiple tasks at the same time on a single display. The user can
+ * perform multiple actions on different tasks simultaneously. Apps open in split screen mode
+ * by default, instead of full screen. Unlike Android's multi-window mode, where users can
+ * choose how to display apps, the device determines how apps are shown.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_CAR_SPLITSCREEN_MULTITASKING =
+ "android.software.car.splitscreen_multitasking";
+
+ /**
* Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature(String, int)}: If this feature is supported, the device supports
* {@link android.security.identity.IdentityCredentialStore} implemented in secure hardware
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index 6b044fc..82694ee 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -97,27 +97,6 @@
public @interface BiometricError {}
/**
- * Single sensor or unspecified multi-sensor behavior (prefer an explicit choice if the
- * device is multi-sensor).
- * @hide
- */
- public static final int BIOMETRIC_MULTI_SENSOR_DEFAULT = 0;
-
- /**
- * Use face and fingerprint sensors together.
- * @hide
- */
- public static final int BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE = 1;
-
- /**
- * @hide
- */
- @IntDef({BIOMETRIC_MULTI_SENSOR_DEFAULT,
- BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE})
- @Retention(RetentionPolicy.SOURCE)
- public @interface BiometricMultiSensorMode {}
-
- /**
* Types of authenticators, defined at a level of granularity supported by
* {@link BiometricManager} and {@link BiometricPrompt}.
*
diff --git a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
index 450c5ce..45f1c8a 100644
--- a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
+++ b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl
@@ -29,5 +29,7 @@
// Notifies the client that an internal event, e.g. back button has occurred.
void onSystemEvent(int event);
// Notifies that the dialog has finished animating.
- void onDialogAnimatedIn();
+ void onDialogAnimatedIn(boolean startFingerprintNow);
+ // Notifies that the fingerprint should start now (after onDialogAnimatedIn(false)).
+ void onStartFingerprintNow();
}
diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java
index 6f2a915..3f40139 100644
--- a/core/java/android/preference/SeekBarVolumizer.java
+++ b/core/java/android/preference/SeekBarVolumizer.java
@@ -37,7 +37,6 @@
import android.os.HandlerThread;
import android.os.Message;
import android.preference.VolumePreference.VolumeStore;
-import android.provider.DeviceConfig;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.provider.Settings.System;
@@ -47,7 +46,6 @@
import android.widget.SeekBar.OnSeekBarChangeListener;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.os.SomeArgs;
import java.util.concurrent.TimeUnit;
@@ -295,14 +293,8 @@
if (zenMuted) {
mSeekBar.setProgress(mLastAudibleStreamVolume, true);
} else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
- /**
- * the first variable above is preserved and the conditions below are made explicit
- * so that when user attempts to slide the notification seekbar out of vibrate the
- * seekbar doesn't wrongly snap back to 0 when the streams aren't aliased
- */
- if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
- || mStreamType == AudioManager.STREAM_RING
+ // For ringer-mode affected streams, show volume as zero when ringermode is vibrate
+ if (mStreamType == AudioManager.STREAM_RING
|| (mStreamType == AudioManager.STREAM_NOTIFICATION && mMuted)) {
mSeekBar.setProgress(0, true);
}
@@ -397,9 +389,7 @@
// set the time of stop volume
if ((mStreamType == AudioManager.STREAM_VOICE_CALL
|| mStreamType == AudioManager.STREAM_RING
- || (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
- && mStreamType == AudioManager.STREAM_NOTIFICATION)
+ || mStreamType == AudioManager.STREAM_NOTIFICATION
|| mStreamType == AudioManager.STREAM_ALARM)) {
sStopVolumeTime = java.lang.System.currentTimeMillis();
}
@@ -686,10 +676,7 @@
}
private void updateVolumeSlider(int streamType, int streamValue) {
- final boolean streamMatch = !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false)
- && mNotificationOrRing ? isNotificationOrRing(streamType) :
- streamType == mStreamType;
+ final boolean streamMatch = (streamType == mStreamType);
if (mSeekBar != null && streamMatch && streamValue != -1) {
final boolean muted = mAudioManager.isStreamMute(mStreamType)
|| streamValue == 0;
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index c0370cc..8c05130 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -493,6 +493,9 @@
if ((flags & FLAG_FIRST_CUSTOM) != 0) {
sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM");
}
+ if ((flags & FLAG_MOVED_TO_TOP) != 0) {
+ sb.append(sb.length() == 0 ? "" : "|").append("MOVE_TO_TOP");
+ }
return sb.toString();
}
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index 7ad2a68..8135f9c 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -549,11 +549,6 @@
"task_manager_inform_job_scheduler_of_pending_app_stop";
/**
- * (boolean) Whether to show notification volume control slider separate from ring.
- */
- public static final String VOLUME_SEPARATE_NOTIFICATION = "volume_separate_notification";
-
- /**
* (boolean) Whether widget provider info would be saved to / loaded from system persistence
* layer as opposed to individual manifests in respective apps.
*/
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index ae58626..d2564fb 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -157,7 +157,7 @@
*/
void showAuthenticationDialog(in PromptInfo promptInfo, IBiometricSysuiReceiver sysuiReceiver,
in int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation, int userId,
- long operationId, String opPackageName, long requestId, int multiSensorConfig);
+ long operationId, String opPackageName, long requestId);
/**
* Used to notify the authentication dialog that a biometric has been authenticated.
*/
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index 3708859..3977666 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -123,8 +123,7 @@
// Used to show the authentication dialog (Biometrics, Device Credential)
void showAuthenticationDialog(in PromptInfo promptInfo, IBiometricSysuiReceiver sysuiReceiver,
in int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
- int userId, long operationId, String opPackageName, long requestId,
- int multiSensorConfig);
+ int userId, long operationId, String opPackageName, long requestId);
// Used to notify the authentication dialog that a biometric has been authenticated
void onBiometricAuthenticated(int modality);
diff --git a/core/proto/android/os/system_properties.proto b/core/proto/android/os/system_properties.proto
index 84c82e0..10f07ac 100644
--- a/core/proto/android/os/system_properties.proto
+++ b/core/proto/android/os/system_properties.proto
@@ -434,9 +434,8 @@
optional string vibrator = 37;
optional string virtual_device = 38;
optional string vulkan = 39;
- optional string egl_legacy = 40;
- // Next Tag: 41
+ // Next Tag: 40
}
optional Hardware hardware = 27;
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f35e32b..f75bcdd 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5129,4 +5129,5 @@
<java-symbol type="style" name="ThemeOverlay.DeviceDefault.Dark.ActionBar.Accent" />
<java-symbol type="drawable" name="focus_event_pressed_key_background" />
+ <java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" />
</resources>
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index c5b00c9..eba7f58 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -33,6 +33,8 @@
import static android.app.Notification.EXTRA_PEOPLE_LIST;
import static android.app.Notification.EXTRA_PICTURE;
import static android.app.Notification.EXTRA_PICTURE_ICON;
+import static android.app.Notification.EXTRA_SUMMARY_TEXT;
+import static android.app.Notification.EXTRA_TITLE;
import static android.app.Notification.MessagingStyle.Message.KEY_DATA_URI;
import static android.app.Notification.MessagingStyle.Message.KEY_SENDER_PERSON;
import static android.app.Notification.MessagingStyle.Message.KEY_TEXT;
@@ -76,6 +78,7 @@
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.SystemProperties;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
@@ -111,6 +114,9 @@
@Before
public void setUp() {
mContext = InstrumentationRegistry.getContext();
+ // TODO(b/169435530): remove this flag set once resolved.
+ SystemProperties.set("persist.sysui.notification.builder_extras_override",
+ Boolean.toString(false));
}
@Test
@@ -1481,6 +1487,107 @@
Assert.assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second);
}
+ // Ensures that extras in a Notification Builder can be updated.
+ @Test
+ public void testExtras_cachedExtrasOverwrittenByUserProvided() {
+ // Sets the flag to new state.
+ // TODO(b/169435530): remove this set value once resolved.
+ SystemProperties.set("persist.sysui.notification.builder_extras_override",
+ Boolean.toString(true));
+ Bundle extras = new Bundle();
+ extras.putCharSequence(EXTRA_TITLE, "test title");
+ extras.putCharSequence(EXTRA_SUMMARY_TEXT, "summary text");
+
+ Notification.Builder builder = new Notification.Builder(mContext, "test id")
+ .addExtras(extras);
+
+ Notification notification = builder.build();
+ assertThat(notification.extras.getCharSequence(EXTRA_TITLE).toString()).isEqualTo(
+ "test title");
+ assertThat(notification.extras.getCharSequence(EXTRA_SUMMARY_TEXT).toString()).isEqualTo(
+ "summary text");
+
+ extras.putCharSequence(EXTRA_TITLE, "new title");
+ builder.addExtras(extras);
+ notification = builder.build();
+ assertThat(notification.extras.getCharSequence(EXTRA_TITLE).toString()).isEqualTo(
+ "new title");
+ assertThat(notification.extras.getCharSequence(EXTRA_SUMMARY_TEXT).toString()).isEqualTo(
+ "summary text");
+ }
+
+ // Ensures that extras in a Notification Builder can be updated by an extender.
+ @Test
+ public void testExtras_cachedExtrasOverwrittenByExtender() {
+ // Sets the flag to new state.
+ // TODO(b/169435530): remove this set value once resolved.
+ SystemProperties.set("persist.sysui.notification.builder_extras_override",
+ Boolean.toString(true));
+ Notification.CarExtender extender = new Notification.CarExtender().setColor(1234);
+
+ Notification notification = new Notification.Builder(mContext, "test id")
+ .extend(extender).build();
+
+ extender.setColor(5678);
+
+ Notification.Builder.recoverBuilder(mContext, notification).extend(extender).build();
+
+ Notification.CarExtender recoveredExtender = new Notification.CarExtender(notification);
+ assertThat(recoveredExtender.getColor()).isEqualTo(5678);
+ }
+
+ // Validates pre-flag flip behavior, that extras in a Notification Builder cannot be updated.
+ // TODO(b/169435530): remove this test once resolved.
+ @Test
+ public void testExtras_cachedExtrasOverwrittenByUserProvidedOld() {
+ // Sets the flag to old state.
+ SystemProperties.set("persist.sysui.notification.builder_extras_override",
+ Boolean.toString(false));
+
+ Bundle extras = new Bundle();
+ extras.putCharSequence(EXTRA_TITLE, "test title");
+ extras.putCharSequence(EXTRA_SUMMARY_TEXT, "summary text");
+
+ Notification.Builder builder = new Notification.Builder(mContext, "test id")
+ .addExtras(extras);
+
+ Notification notification = builder.build();
+ assertThat(notification.extras.getCharSequence(EXTRA_TITLE).toString()).isEqualTo(
+ "test title");
+ assertThat(notification.extras.getCharSequence(EXTRA_SUMMARY_TEXT).toString()).isEqualTo(
+ "summary text");
+
+ extras.putCharSequence(EXTRA_TITLE, "new title");
+ builder.addExtras(extras);
+ notification = builder.build();
+ assertThat(notification.extras.getCharSequence(EXTRA_TITLE).toString()).isEqualTo(
+ "test title");
+ assertThat(notification.extras.getCharSequence(EXTRA_SUMMARY_TEXT).toString()).isEqualTo(
+ "summary text");
+ }
+
+ // Validates pre-flag flip behavior, that extras in a Notification Builder cannot be updated
+ // by an extender.
+ // TODO(b/169435530): remove this test once resolved.
+ @Test
+ public void testExtras_cachedExtrasOverwrittenByExtenderOld() {
+ // Sets the flag to old state.
+ SystemProperties.set("persist.sysui.notification.builder_extras_override",
+ Boolean.toString(false));
+
+ Notification.CarExtender extender = new Notification.CarExtender().setColor(1234);
+
+ Notification notification = new Notification.Builder(mContext, "test id")
+ .extend(extender).build();
+
+ extender.setColor(5678);
+
+ Notification.Builder.recoverBuilder(mContext, notification).extend(extender).build();
+
+ Notification.CarExtender recoveredExtender = new Notification.CarExtender(notification);
+ assertThat(recoveredExtender.getColor()).isEqualTo(1234);
+ }
+
private void assertValid(Notification.Colors c) {
// Assert that all colors are populated
assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index fbdbd3e..7b37d59 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.activityembedding;
+import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION;
import static android.window.TransitionInfo.FLAG_FILLS_TASK;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
@@ -111,6 +112,11 @@
if (containsNonEmbeddedChange && !handleNonEmbeddedChanges(changes)) {
return false;
}
+ final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+ if (options != null && options.getType() == ANIM_SCENE_TRANSITION) {
+ // Scene-transition will be handled by app side.
+ return false;
+ }
// Start ActivityEmbedding animation.
mTransitionCallbacks.put(transition, finishCallback);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
index ed8dc7de..fc674a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithm.java
@@ -65,9 +65,18 @@
}
Rect pipBounds = new Rect(startingBounds);
- // move PiP towards corner if user hasn't moved it manually or the flag is on
- if (mKeepClearAreaGravityEnabled
- || (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip())) {
+ boolean shouldApplyGravity = false;
+ // if PiP is outside of screen insets, reposition using gravity
+ if (!insets.contains(pipBounds)) {
+ shouldApplyGravity = true;
+ }
+ // if user has not interacted with PiP, reposition using gravity
+ if (!pipBoundsState.hasUserMovedPip() && !pipBoundsState.hasUserResizedPip()) {
+ shouldApplyGravity = true;
+ }
+
+ // apply gravity that will position PiP in bottom left or bottom right corner within insets
+ if (mKeepClearAreaGravityEnabled || shouldApplyGravity) {
float snapFraction = pipBoundsAlgorithm.getSnapFraction(startingBounds);
int verticalGravity = Gravity.BOTTOM;
int horizontalGravity;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index f33b077..d16b497 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -492,6 +492,10 @@
finishT.show(leash);
} else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
finishT.hide(leash);
+ } else if (isOpening && mode == TRANSIT_CHANGE) {
+ // Just in case there is a race with another animation (eg. recents finish()).
+ // Changes are visible->visible so it's a problem if it isn't visible.
+ t.show(leash);
}
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 0db88af..0c1b793 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -51,6 +51,7 @@
CsipDeviceManager mCsipDeviceManager;
BluetoothDevice mOngoingSetMemberPair;
boolean mIsLateBonding;
+ int mGroupIdOfLateBonding;
public CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) {
mContext = context;
@@ -213,6 +214,14 @@
* @return The name, or if unavailable, the address.
*/
public String getName(BluetoothDevice device) {
+ if (isOngoingPairByCsip(device)) {
+ CachedBluetoothDevice firstDevice =
+ mCsipDeviceManager.getFirstMemberDevice(mGroupIdOfLateBonding);
+ if (firstDevice != null && firstDevice.getName() != null) {
+ return firstDevice.getName();
+ }
+ }
+
CachedBluetoothDevice cachedDevice = findDevice(device);
if (cachedDevice != null && cachedDevice.getName() != null) {
return cachedDevice.getName();
@@ -314,6 +323,7 @@
// To clear the SetMemberPair flag when the Bluetooth is turning off.
mOngoingSetMemberPair = null;
mIsLateBonding = false;
+ mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
}
@@ -426,6 +436,7 @@
return false;
}
+ Log.d(TAG, "isLateBonding: " + mIsLateBonding);
return mIsLateBonding;
}
@@ -444,11 +455,13 @@
Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " groupId=" + groupId + " by CSIP ");
mOngoingSetMemberPair = device;
mIsLateBonding = checkLateBonding(groupId);
+ mGroupIdOfLateBonding = groupId;
syncConfigFromMainDevice(device, groupId);
if (!device.createBond(BluetoothDevice.TRANSPORT_LE)) {
Log.d(TAG, "Bonding could not be started");
mOngoingSetMemberPair = null;
mIsLateBonding = false;
+ mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
}
@@ -494,6 +507,7 @@
mOngoingSetMemberPair = null;
mIsLateBonding = false;
+ mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
if (bondState != BluetoothDevice.BOND_NONE) {
if (findDevice(device) == null) {
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml
index b3b40f3..8169189 100644
--- a/packages/SystemUI/res/layout/auth_biometric_contents.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml
@@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+<!-- TODO(b/251476085): inline in biometric_prompt_layout after Biometric*Views are un-flagged -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
diff --git a/packages/SystemUI/res/layout/auth_biometric_face_view.xml b/packages/SystemUI/res/layout/auth_biometric_face_view.xml
index be30f21..e3d0732 100644
--- a/packages/SystemUI/res/layout/auth_biometric_face_view.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_face_view.xml
@@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+<!-- TODO(b/251476085): remove after BiometricFaceView is un-flagged -->
<com.android.systemui.biometrics.AuthBiometricFaceView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
diff --git a/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml b/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml
index 05ca2a7..896d836 100644
--- a/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml
@@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+<!-- TODO(b/251476085): remove after BiometricFingerprintAndFaceView is un-flagged -->
<com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
diff --git a/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml b/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
index 01ea31f..e36f9796 100644
--- a/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_fingerprint_view.xml
@@ -13,7 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+<!-- TODO(b/251476085): remove after BiometricFingerprintView is un-flagged -->
<com.android.systemui.biometrics.AuthBiometricFingerprintView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contents"
diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
new file mode 100644
index 0000000..05ff1b1
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
@@ -0,0 +1,176 @@
+<!--
+ ~ 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.
+ -->
+<com.android.systemui.biometrics.ui.BiometricPromptLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/contents"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="1"
+ android:ellipsize="marquee"
+ android:importantForAccessibility="no"
+ style="@style/TextAppearance.AuthCredential.Title"/>
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="1"
+ android:ellipsize="marquee"
+ style="@style/TextAppearance.AuthCredential.Subtitle"/>
+
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbars ="vertical"
+ android:importantForAccessibility="no"
+ style="@style/TextAppearance.AuthCredential.Description"/>
+
+ <Space android:id="@+id/space_above_icon"
+ android:layout_width="match_parent"
+ android:layout_height="48dp" />
+
+ <FrameLayout
+ android:id="@+id/biometric_icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center">
+
+ <com.airbnb.lottie.LottieAnimationView
+ android:id="@+id/biometric_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:contentDescription="@null"
+ android:scaleType="fitXY" />
+
+ <com.airbnb.lottie.LottieAnimationView
+ android:id="@+id/biometric_icon_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:contentDescription="@null"
+ android:scaleType="fitXY" />
+ </FrameLayout>
+
+ <!-- For sensors such as UDFPS, this view is used during custom measurement/layout to add extra
+ padding so that the biometric icon is always in the right physical position. -->
+ <Space android:id="@+id/space_below_icon"
+ android:layout_width="match_parent"
+ android:layout_height="12dp" />
+
+ <TextView
+ android:id="@+id/indicator"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="24dp"
+ android:textSize="12sp"
+ android:gravity="center_horizontal"
+ android:accessibilityLiveRegion="polite"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:fadingEdge="horizontal"
+ android:textColor="@color/biometric_dialog_gray"/>
+
+ <LinearLayout
+ android:id="@+id/button_bar"
+ android:layout_width="match_parent"
+ android:layout_height="88dp"
+ style="?android:attr/buttonBarStyle"
+ android:orientation="horizontal"
+ android:paddingTop="24dp">
+
+ <Space android:id="@+id/leftSpacer"
+ android:layout_width="8dp"
+ android:layout_height="match_parent"
+ android:visibility="visible" />
+
+ <!-- Negative Button, reserved for app -->
+ <Button android:id="@+id/button_negative"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+ android:visibility="gone"/>
+ <!-- Cancel Button, replaces negative button when biometric is accepted -->
+ <Button android:id="@+id/button_cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_gravity="center_vertical"
+ android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+ android:text="@string/cancel"
+ android:visibility="gone"/>
+ <!-- "Use Credential" Button, replaces if device credential is allowed -->
+ <Button android:id="@+id/button_use_credential"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_gravity="center_vertical"
+ android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
+ android:visibility="gone"/>
+
+ <Space android:id="@+id/middleSpacer"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:visibility="visible"/>
+
+ <!-- Positive Button -->
+ <Button android:id="@+id/button_confirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Colored"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:maxWidth="@dimen/biometric_dialog_button_positive_max_width"
+ android:text="@string/biometric_dialog_confirm"
+ android:visibility="gone"/>
+ <!-- Try Again Button -->
+ <Button android:id="@+id/button_try_again"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@*android:style/Widget.DeviceDefault.Button.Colored"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:maxWidth="@dimen/biometric_dialog_button_positive_max_width"
+ android:text="@string/biometric_dialog_try_again"
+ android:visibility="gone"/>
+
+ <Space android:id="@+id/rightSpacer"
+ android:layout_width="8dp"
+ android:layout_height="match_parent"
+ android:visibility="visible" />
+ </LinearLayout>
+
+</com.android.systemui.biometrics.ui.BiometricPromptLayout>
diff --git a/packages/SystemUI/res/layout/screen_record_dialog.xml b/packages/SystemUI/res/layout/screen_record_dialog.xml
index ae052502..bbf3adf 100644
--- a/packages/SystemUI/res/layout/screen_record_dialog.xml
+++ b/packages/SystemUI/res/layout/screen_record_dialog.xml
@@ -73,7 +73,7 @@
android:tint="?android:attr/textColorSecondary"
android:layout_gravity="center"
android:layout_weight="0"
- android:layout_marginRight="@dimen/screenrecord_option_padding"/>
+ android:layout_marginEnd="@dimen/screenrecord_option_padding"/>
<Spinner
android:id="@+id/screen_recording_options"
android:layout_width="0dp"
@@ -106,7 +106,7 @@
android:src="@drawable/ic_touch"
android:tint="?android:attr/textColorSecondary"
android:layout_gravity="center"
- android:layout_marginRight="@dimen/screenrecord_option_padding"/>
+ android:layout_marginEnd="@dimen/screenrecord_option_padding"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/volume_ringer_drawer.xml b/packages/SystemUI/res/layout/volume_ringer_drawer.xml
index 1112bcd..9b1fa23 100644
--- a/packages/SystemUI/res/layout/volume_ringer_drawer.xml
+++ b/packages/SystemUI/res/layout/volume_ringer_drawer.xml
@@ -85,7 +85,7 @@
android:layout_height="@dimen/volume_ringer_drawer_icon_size"
android:layout_gravity="center"
android:tint="?android:attr/textColorPrimary"
- android:src="@drawable/ic_volume_ringer_mute" />
+ android:src="@drawable/ic_speaker_mute" />
</FrameLayout>
@@ -102,7 +102,7 @@
android:layout_height="@dimen/volume_ringer_drawer_icon_size"
android:layout_gravity="center"
android:tint="?android:attr/textColorPrimary"
- android:src="@drawable/ic_volume_ringer" />
+ android:src="@drawable/ic_speaker_on" />
</FrameLayout>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 003f9b0..67fdb4c 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -353,7 +353,7 @@
<!-- Message shown when a biometric is authenticated, waiting for the user to confirm authentication [CHAR LIMIT=40]-->
<string name="biometric_dialog_tap_confirm">Tap Confirm to complete</string>
<!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
- <string name="biometric_dialog_tap_confirm_with_face">Unlocked by face. Press the unlock icon to continue.</string>
+ <string name="biometric_dialog_tap_confirm_with_face">Unlocked by face.</string>
<!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
<string name="biometric_dialog_tap_confirm_with_face_alt_1">Unlocked by face. Press to continue.</string>
<!-- Message shown when a biometric has authenticated with a user's face and is waiting for the user to confirm authentication [CHAR LIMIT=60]-->
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 9d9a87d..c684dc5 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -51,6 +51,12 @@
*/
val isBypassEnabled: StateFlow<Boolean>
+ /**
+ * Number of consecutively failed authentication attempts. This resets to `0` when
+ * authentication succeeds.
+ */
+ val failedAuthenticationAttempts: StateFlow<Int>
+
/** See [isUnlocked]. */
fun setUnlocked(isUnlocked: Boolean)
@@ -59,6 +65,9 @@
/** See [isBypassEnabled]. */
fun setBypassEnabled(isBypassEnabled: Boolean)
+
+ /** See [failedAuthenticationAttempts]. */
+ fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int)
}
class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
@@ -75,6 +84,10 @@
private val _isBypassEnabled = MutableStateFlow(false)
override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
+ private val _failedAuthenticationAttempts = MutableStateFlow(0)
+ override val failedAuthenticationAttempts: StateFlow<Int> =
+ _failedAuthenticationAttempts.asStateFlow()
+
override fun setUnlocked(isUnlocked: Boolean) {
_isUnlocked.value = isUnlocked
}
@@ -86,6 +99,10 @@
override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
_authenticationMethod.value = authenticationMethod
}
+
+ override fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int) {
+ _failedAuthenticationAttempts.value = failedAuthenticationAttempts
+ }
}
@Module
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 5aea930..3984627 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -75,6 +75,12 @@
*/
val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
+ /**
+ * Number of consecutively failed authentication attempts. This resets to `0` when
+ * authentication succeeds.
+ */
+ val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts
+
init {
// UNLOCKS WHEN AUTH METHOD REMOVED.
//
@@ -130,7 +136,12 @@
}
if (isSuccessful) {
+ repository.setFailedAuthenticationAttempts(0)
repository.setUnlocked(true)
+ } else {
+ repository.setFailedAuthenticationAttempts(
+ repository.failedAuthenticationAttempts.value + 1
+ )
}
return isSuccessful
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
index 83250b6..6f008c3 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt
@@ -36,8 +36,10 @@
data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)
- data class Pattern(val coordinates: List<PatternCoordinate>) :
- AuthenticationMethodModel(isSecure = true) {
+ data class Pattern(
+ val coordinates: List<PatternCoordinate>,
+ val isPatternVisible: Boolean = true,
+ ) : AuthenticationMethodModel(isSecure = true) {
data class PatternCoordinate(
val x: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
index 1404053..682888f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt
@@ -21,42 +21,43 @@
import com.airbnb.lottie.LottieAnimationView
import com.android.systemui.R
import com.android.systemui.biometrics.AuthBiometricView.BiometricState
-import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED
import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR
import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP
import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION
/** Face/Fingerprint combined icon animator for BiometricPrompt. */
-class AuthBiometricFingerprintAndFaceIconController(
- context: Context,
- iconView: LottieAnimationView,
- iconViewOverlay: LottieAnimationView
+open class AuthBiometricFingerprintAndFaceIconController(
+ context: Context,
+ iconView: LottieAnimationView,
+ iconViewOverlay: LottieAnimationView,
) : AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) {
override val actsAsConfirmButton: Boolean = true
override fun shouldAnimateIconViewForTransition(
- @BiometricState oldState: Int,
- @BiometricState newState: Int
+ @BiometricState oldState: Int,
+ @BiometricState newState: Int
): Boolean = when (newState) {
STATE_PENDING_CONFIRMATION -> true
- STATE_AUTHENTICATED -> false
else -> super.shouldAnimateIconViewForTransition(oldState, newState)
}
@RawRes
override fun getAnimationForTransition(
- @BiometricState oldState: Int,
- @BiometricState newState: Int
+ @BiometricState oldState: Int,
+ @BiometricState newState: Int
): Int? = when (newState) {
STATE_PENDING_CONFIRMATION -> {
if (oldState == STATE_ERROR || oldState == STATE_HELP) {
R.raw.fingerprint_dialogue_error_to_unlock_lottie
+ } else if (oldState == STATE_PENDING_CONFIRMATION) {
+ // TODO(jbolinger): missing asset for this transition
+ // (unlocked icon to success checkmark)
+ R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie
} else {
R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie
}
}
- STATE_AUTHENTICATED -> null
else -> super.getAnimationForTransition(oldState, newState)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt
index 57ffd24..7ce74db 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt
@@ -40,11 +40,11 @@
override fun ignoreUnsuccessfulEventsFrom(@Modality modality: Int, unsuccessfulReason: String) =
modality == TYPE_FACE && !(isFaceClass3 && isLockoutErrorString(unsuccessfulReason))
- override fun onPointerDown(failedModalities: Set<Int>) = failedModalities.contains(TYPE_FACE)
-
override fun createIconController(): AuthIconController =
AuthBiometricFingerprintAndFaceIconController(mContext, mIconView, mIconViewOverlay)
+ override fun isCoex() = true
+
private fun isLockoutErrorString(unsuccessfulReason: String) =
unsuccessfulReason == FaceManager.getErrorString(
mContext,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 4db371b..fb160f2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -56,43 +56,42 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
-import java.util.Set;
/**
* Contains the Biometric views (title, subtitle, icon, buttons, etc.) and its controllers.
*/
-public abstract class AuthBiometricView extends LinearLayout {
+public abstract class AuthBiometricView extends LinearLayout implements AuthBiometricViewAdapter {
private static final String TAG = "AuthBiometricView";
/**
* Authentication hardware idle.
*/
- protected static final int STATE_IDLE = 0;
+ public static final int STATE_IDLE = 0;
/**
* UI animating in, authentication hardware active.
*/
- protected static final int STATE_AUTHENTICATING_ANIMATING_IN = 1;
+ public static final int STATE_AUTHENTICATING_ANIMATING_IN = 1;
/**
* UI animated in, authentication hardware active.
*/
- protected static final int STATE_AUTHENTICATING = 2;
+ public static final int STATE_AUTHENTICATING = 2;
/**
* UI animated in, authentication hardware active.
*/
- protected static final int STATE_HELP = 3;
+ public static final int STATE_HELP = 3;
/**
* Hard error, e.g. ERROR_TIMEOUT. Authentication hardware idle.
*/
- protected static final int STATE_ERROR = 4;
+ public static final int STATE_ERROR = 4;
/**
* Authenticated, waiting for user confirmation. Authentication hardware idle.
*/
- protected static final int STATE_PENDING_CONFIRMATION = 5;
+ public static final int STATE_PENDING_CONFIRMATION = 5;
/**
* Authenticated, dialog animating away soon.
*/
- protected static final int STATE_AUTHENTICATED = 6;
+ public static final int STATE_AUTHENTICATED = 6;
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_IDLE, STATE_AUTHENTICATING_ANIMATING_IN, STATE_AUTHENTICATING, STATE_HELP,
@@ -102,13 +101,14 @@
/**
* Callback to the parent when a user action has occurred.
*/
- interface Callback {
+ public interface Callback {
int ACTION_AUTHENTICATED = 1;
int ACTION_USER_CANCELED = 2;
int ACTION_BUTTON_NEGATIVE = 3;
int ACTION_BUTTON_TRY_AGAIN = 4;
int ACTION_ERROR = 5;
int ACTION_USE_DEVICE_CREDENTIAL = 6;
+ int ACTION_START_DELAYED_FINGERPRINT_SENSOR = 7;
/**
* When an action has occurred. The caller will only invoke this when the callback should
@@ -268,6 +268,27 @@
/** Create the controller for managing the icons transitions during the prompt.*/
@NonNull
protected abstract AuthIconController createIconController();
+
+ @Override
+ public AuthIconController getLegacyIconController() {
+ return mIconController;
+ }
+
+ @Override
+ public void cancelAnimation() {
+ animate().cancel();
+ }
+
+ @Override
+ public View asView() {
+ return this;
+ }
+
+ @Override
+ public boolean isCoex() {
+ return false;
+ }
+
void setPanelController(AuthPanelController panelController) {
mPanelController = panelController;
}
@@ -544,12 +565,12 @@
mState = newState;
}
- void onOrientationChanged() {
+ public void onOrientationChanged() {
// Update padding and AuthPanel outline by calling updateSize when the orientation changed.
updateSize(mSize);
}
- public void onDialogAnimatedIn() {
+ public void onDialogAnimatedIn(boolean fingerprintWasStarted) {
updateState(STATE_AUTHENTICATING);
}
@@ -597,18 +618,6 @@
}
/**
- * Fingerprint pointer down event. This does nothing by default and will not be called if the
- * device does not have an appropriate sensor (UDFPS), but it may be used as an alternative
- * to the "retry" button when fingerprint is used with other modalities.
- *
- * @param failedModalities the set of modalities that have failed
- * @return true if a retry was initiated as a result of this event
- */
- public boolean onPointerDown(Set<Integer> failedModalities) {
- return false;
- }
-
- /**
* Show a help message to the user.
*
* @param modality sensor modality
@@ -752,7 +761,8 @@
/**
* Kicks off the animation process and invokes the callback.
*/
- void startTransitionToCredentialUI() {
+ @Override
+ public void startTransitionToCredentialUI() {
updateSize(AuthDialog.SIZE_LARGE);
mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt
new file mode 100644
index 0000000..631511c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 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
+
+import android.hardware.biometrics.BiometricAuthenticator
+import android.os.Bundle
+import android.view.View
+
+/** TODO(b/251476085): Temporary interface while legacy biometric prompt is around. */
+@Deprecated("temporary adapter while migrating biometric prompt - do not expand")
+interface AuthBiometricViewAdapter {
+ val legacyIconController: AuthIconController?
+
+ fun onDialogAnimatedIn(fingerprintWasStarted: Boolean)
+
+ fun onAuthenticationSucceeded(@BiometricAuthenticator.Modality modality: Int)
+
+ fun onAuthenticationFailed(
+ @BiometricAuthenticator.Modality modality: Int,
+ failureReason: String
+ )
+
+ fun onError(@BiometricAuthenticator.Modality modality: Int, error: String)
+
+ fun onHelp(@BiometricAuthenticator.Modality modality: Int, help: String)
+
+ fun startTransitionToCredentialUI()
+
+ fun requestLayout()
+
+ fun onSaveState(bundle: Bundle?)
+
+ fun restoreState(bundle: Bundle?)
+
+ fun onOrientationChanged()
+
+ fun cancelAnimation()
+
+ fun isCoex(): Boolean
+
+ fun asView(): View
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index e775c2e..49ac264 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -16,15 +16,13 @@
package com.android.systemui.biometrics;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
-import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
import android.animation.Animator;
-import android.annotation.DurationMillisLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -66,12 +64,19 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.R;
import com.android.systemui.biometrics.AuthController.ScaleFactorProvider;
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
+import com.android.systemui.biometrics.domain.model.BiometricModalities;
+import com.android.systemui.biometrics.ui.BiometricPromptLayout;
import com.android.systemui.biometrics.ui.CredentialView;
import com.android.systemui.biometrics.ui.binder.AuthBiometricFingerprintViewBinder;
+import com.android.systemui.biometrics.ui.binder.BiometricViewBinder;
import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -84,6 +89,8 @@
import javax.inject.Provider;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* Top level container/controller for the BiometricPrompt UI.
*/
@@ -126,16 +133,20 @@
private final WakefulnessLifecycle mWakefulnessLifecycle;
private final AuthDialogPanelInteractionDetector mPanelInteractionDetector;
private final InteractionJankMonitor mInteractionJankMonitor;
+ private final CoroutineScope mApplicationCoroutineScope;
// TODO: these should be migrated out once ready
- private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor;
+ private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor;
private final Provider<AuthBiometricFingerprintViewModel>
mAuthBiometricFingerprintViewModelProvider;
+ private final @NonNull Provider<PromptSelectorInteractor> mPromptSelectorInteractorProvider;
+ // TODO(b/251476085): these should be migrated out of the view
private final Provider<CredentialViewModel> mCredentialViewModelProvider;
+ private final PromptViewModel mPromptViewModel;
@VisibleForTesting final BiometricCallback mBiometricCallback;
- @Nullable private AuthBiometricView mBiometricView;
+ @Nullable private AuthBiometricViewAdapter mBiometricView;
@Nullable private View mCredentialView;
private final AuthPanelController mPanelController;
private final FrameLayout mFrameLayout;
@@ -154,7 +165,8 @@
// HAT received from LockSettingsService when credential is verified.
@Nullable private byte[] mCredentialAttestation;
- @VisibleForTesting
+ // TODO(b/251476085): remove when legacy prompt is replaced
+ @Deprecated
static class Config {
Context mContext;
AuthDialogCallback mCallback;
@@ -167,96 +179,9 @@
long mOperationId;
long mRequestId = -1;
boolean mSkipAnimation = false;
- @BiometricMultiSensorMode int mMultiSensorConfig = BIOMETRIC_MULTI_SENSOR_DEFAULT;
ScaleFactorProvider mScaleProvider;
}
- public static class Builder {
- Config mConfig;
-
- public Builder(Context context) {
- mConfig = new Config();
- mConfig.mContext = context;
- }
-
- public Builder setCallback(AuthDialogCallback callback) {
- mConfig.mCallback = callback;
- return this;
- }
-
- public Builder setPromptInfo(PromptInfo promptInfo) {
- mConfig.mPromptInfo = promptInfo;
- return this;
- }
-
- public Builder setRequireConfirmation(boolean requireConfirmation) {
- mConfig.mRequireConfirmation = requireConfirmation;
- return this;
- }
-
- public Builder setUserId(int userId) {
- mConfig.mUserId = userId;
- return this;
- }
-
- public Builder setOpPackageName(String opPackageName) {
- mConfig.mOpPackageName = opPackageName;
- return this;
- }
-
- public Builder setSkipIntro(boolean skip) {
- mConfig.mSkipIntro = skip;
- return this;
- }
-
- public Builder setOperationId(@DurationMillisLong long operationId) {
- mConfig.mOperationId = operationId;
- return this;
- }
-
- /** Unique id for this request. */
- public Builder setRequestId(long requestId) {
- mConfig.mRequestId = requestId;
- return this;
- }
-
- @VisibleForTesting
- public Builder setSkipAnimationDuration(boolean skip) {
- mConfig.mSkipAnimation = skip;
- return this;
- }
-
- /** The multi-sensor mode. */
- public Builder setMultiSensorConfig(@BiometricMultiSensorMode int multiSensorConfig) {
- mConfig.mMultiSensorConfig = multiSensorConfig;
- return this;
- }
-
- public Builder setScaleFactorProvider(ScaleFactorProvider scaleProvider) {
- mConfig.mScaleProvider = scaleProvider;
- return this;
- }
-
- public AuthContainerView build(@Background DelayableExecutor bgExecutor, int[] sensorIds,
- @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
- @Nullable List<FaceSensorPropertiesInternal> faceProps,
- @NonNull WakefulnessLifecycle wakefulnessLifecycle,
- @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
- @NonNull UserManager userManager,
- @NonNull LockPatternUtils lockPatternUtils,
- @NonNull InteractionJankMonitor jankMonitor,
- @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
- @NonNull Provider<AuthBiometricFingerprintViewModel>
- authBiometricFingerprintViewModelProvider,
- @NonNull Provider<CredentialViewModel> credentialViewModelProvider) {
- mConfig.mSensorIds = sensorIds;
- return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle,
- panelInteractionDetector, userManager, lockPatternUtils, jankMonitor,
- biometricPromptInteractor, authBiometricFingerprintViewModelProvider,
- credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor);
- }
- }
-
@VisibleForTesting
final class BiometricCallback implements AuthBiometricView.Callback {
@Override
@@ -285,6 +210,9 @@
addCredentialView(false /* animatePanel */, true /* animateContents */);
}, mConfig.mSkipAnimation ? 0 : AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS);
break;
+ case AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR:
+ mConfig.mCallback.onStartFingerprintNow(getRequestId());
+ break;
default:
Log.e(TAG, "Unhandled action: " + action);
}
@@ -336,8 +264,10 @@
alertDialog.show();
}
- @VisibleForTesting
- AuthContainerView(Config config,
+ // TODO(b/251476085): remove Config and further decompose these properties out of view classes
+ AuthContainerView(@NonNull Config config,
+ @NonNull FeatureFlags featureFlags,
+ @NonNull CoroutineScope applicationCoroutineScope,
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@@ -345,9 +275,36 @@
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull InteractionJankMonitor jankMonitor,
- @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
@NonNull Provider<AuthBiometricFingerprintViewModel>
authBiometricFingerprintViewModelProvider,
+ @NonNull Provider<PromptCredentialInteractor> promptCredentialInteractor,
+ @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor,
+ @NonNull PromptViewModel promptViewModel,
+ @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
+ @NonNull @Background DelayableExecutor bgExecutor) {
+ this(config, featureFlags, applicationCoroutineScope, fpProps, faceProps,
+ wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
+ jankMonitor, authBiometricFingerprintViewModelProvider, promptSelectorInteractor,
+ promptCredentialInteractor, promptViewModel, credentialViewModelProvider,
+ new Handler(Looper.getMainLooper()), bgExecutor);
+ }
+
+ @VisibleForTesting
+ AuthContainerView(@NonNull Config config,
+ @NonNull FeatureFlags featureFlags,
+ @NonNull CoroutineScope applicationCoroutineScope,
+ @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
+ @Nullable List<FaceSensorPropertiesInternal> faceProps,
+ @NonNull WakefulnessLifecycle wakefulnessLifecycle,
+ @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
+ @NonNull UserManager userManager,
+ @NonNull LockPatternUtils lockPatternUtils,
+ @NonNull InteractionJankMonitor jankMonitor,
+ @NonNull Provider<AuthBiometricFingerprintViewModel>
+ authBiometricFingerprintViewModelProvider,
+ @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
+ @NonNull Provider<PromptCredentialInteractor> credentialInteractor,
+ @NonNull PromptViewModel promptViewModel,
@NonNull Provider<CredentialViewModel> credentialViewModelProvider,
@NonNull Handler mainHandler,
@NonNull @Background DelayableExecutor bgExecutor) {
@@ -360,6 +317,7 @@
mWindowManager = mContext.getSystemService(WindowManager.class);
mWakefulnessLifecycle = wakefulnessLifecycle;
mPanelInteractionDetector = panelInteractionDetector;
+ mApplicationCoroutineScope = applicationCoroutineScope;
mTranslationY = getResources()
.getDimension(R.dimen.biometric_dialog_animation_translation_offset);
@@ -376,10 +334,70 @@
mPanelController = new AuthPanelController(mContext, mPanelView);
mBackgroundExecutor = bgExecutor;
mInteractionJankMonitor = jankMonitor;
- mBiometricPromptInteractor = biometricPromptInteractor;
+ mPromptCredentialInteractor = credentialInteractor;
mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider;
+ mPromptSelectorInteractorProvider = promptSelectorInteractorProvider;
mCredentialViewModelProvider = credentialViewModelProvider;
+ mPromptViewModel = promptViewModel;
+ if (featureFlags.isEnabled(Flags.BIOMETRIC_BP_STRONG)) {
+ showPrompt(config, layoutInflater, promptViewModel,
+ Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
+ Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds));
+ } else {
+ showLegacyPrompt(config, layoutInflater, fpProps, faceProps);
+ }
+
+ // TODO: De-dupe the logic with AuthCredentialPasswordView
+ setOnKeyListener((v, keyCode, event) -> {
+ if (keyCode != KeyEvent.KEYCODE_BACK) {
+ return false;
+ }
+ if (event.getAction() == KeyEvent.ACTION_UP) {
+ onBackInvoked();
+ }
+ return true;
+ });
+
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ setFocusableInTouchMode(true);
+ requestFocus();
+ }
+
+ private void showPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
+ @NonNull PromptViewModel viewModel,
+ @Nullable FingerprintSensorPropertiesInternal fpProps,
+ @Nullable FaceSensorPropertiesInternal faceProps) {
+ if (Utils.isBiometricAllowed(config.mPromptInfo)) {
+ mPromptSelectorInteractorProvider.get().useBiometricsForAuthentication(
+ config.mPromptInfo,
+ config.mRequireConfirmation,
+ config.mUserId,
+ config.mOperationId,
+ new BiometricModalities(fpProps, faceProps));
+
+ final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate(
+ R.layout.biometric_prompt_layout, null, false);
+ mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
+ // TODO(b/201510778): This uses the wrong timeout in some cases
+ getJankListener(view, TRANSIT, AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
+ mBackgroundView, mBiometricCallback, mApplicationCoroutineScope);
+
+ // TODO(b/251476085): migrate these dependencies
+ if (fpProps != null && fpProps.isAnyUdfpsType()) {
+ view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps),
+ config.mScaleProvider);
+ }
+ } else {
+ mPromptSelectorInteractorProvider.get().resetPrompt();
+ }
+ }
+
+ // TODO(b/251476085): remove entirely
+ private void showLegacyPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
+ @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
+ @Nullable List<FaceSensorPropertiesInternal> faceProps
+ ) {
// Inflate biometric view only if necessary.
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
final FingerprintSensorPropertiesInternal fpProperties =
@@ -421,31 +439,18 @@
// init view before showing
if (mBiometricView != null) {
- mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation);
- mBiometricView.setPanelController(mPanelController);
- mBiometricView.setPromptInfo(mConfig.mPromptInfo);
- mBiometricView.setCallback(mBiometricCallback);
- mBiometricView.setBackgroundView(mBackgroundView);
- mBiometricView.setUserId(mConfig.mUserId);
- mBiometricView.setEffectiveUserId(mEffectiveUserId);
- mBiometricView.setJankListener(getJankListener(mBiometricView, TRANSIT,
+ final AuthBiometricView view = (AuthBiometricView) mBiometricView;
+ view.setRequireConfirmation(mConfig.mRequireConfirmation);
+ view.setPanelController(mPanelController);
+ view.setPromptInfo(mConfig.mPromptInfo);
+ view.setCallback(mBiometricCallback);
+ view.setBackgroundView(mBackgroundView);
+ view.setUserId(mConfig.mUserId);
+ view.setEffectiveUserId(mEffectiveUserId);
+ // TODO(b/201510778): This uses the wrong timeout in some cases (remove w/ above)
+ view.setJankListener(getJankListener(view, TRANSIT,
AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS));
}
-
- // TODO: De-dupe the logic with AuthCredentialPasswordView
- setOnKeyListener((v, keyCode, event) -> {
- if (keyCode != KeyEvent.KEYCODE_BACK) {
- return false;
- }
- if (event.getAction() == KeyEvent.ACTION_UP) {
- onBackInvoked();
- }
- return true;
- });
-
- setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- setFocusableInTouchMode(true);
- requestFocus();
}
private void onBackInvoked() {
@@ -495,7 +500,7 @@
mBackgroundView.setOnClickListener(null);
mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- mBiometricPromptInteractor.get().useCredentialsForAuthentication(
+ mPromptSelectorInteractorProvider.get().useCredentialsForAuthentication(
mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId);
final CredentialViewModel vm = mCredentialViewModelProvider.get();
vm.setAnimateContents(animateContents);
@@ -527,7 +532,7 @@
() -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED));
if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
- mBiometricScrollView.addView(mBiometricView);
+ mBiometricScrollView.addView(mBiometricView.asView());
} else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
addCredentialView(true /* animatePanel */, false /* animateContents */);
} else {
@@ -601,9 +606,13 @@
}
private static boolean shouldUpdatePositionForUdfps(@NonNull View view) {
+ // TODO(b/251476085): legacy view (delete when removed)
if (view instanceof AuthBiometricFingerprintView) {
return ((AuthBiometricFingerprintView) view).isUdfps();
}
+ if (view instanceof BiometricPromptLayout) {
+ return ((BiometricPromptLayout) view).isUdfps();
+ }
return false;
}
@@ -613,7 +622,7 @@
if (display == null) {
return false;
}
- if (!shouldUpdatePositionForUdfps(mBiometricView)) {
+ if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) {
return false;
}
@@ -626,12 +635,12 @@
case Surface.ROTATION_90:
mPanelController.setPosition(AuthPanelController.POSITION_RIGHT);
- setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
+ setScrollViewGravity(Gravity.BOTTOM | Gravity.RIGHT);
break;
case Surface.ROTATION_270:
mPanelController.setPosition(AuthPanelController.POSITION_LEFT);
- setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
+ setScrollViewGravity(Gravity.BOTTOM | Gravity.LEFT);
break;
case Surface.ROTATION_180:
@@ -689,7 +698,7 @@
mCredentialView.animate().cancel();
}
mPanelView.animate().cancel();
- mBiometricView.animate().cancel();
+ mBiometricView.cancelAnimation();
animate().cancel();
onDialogAnimatedIn();
}
@@ -750,8 +759,9 @@
@Override
public void onPointerDown() {
if (mBiometricView != null) {
- if (mBiometricView.onPointerDown(mFailedModalities)) {
+ if (mFailedModalities.contains(TYPE_FACE)) {
Log.d(TAG, "retrying failed modalities (pointer down)");
+ mFailedModalities.remove(TYPE_FACE);
mBiometricCallback.onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN);
}
} else {
@@ -885,11 +895,17 @@
}
mContainerState = STATE_SHOWING;
if (mBiometricView != null) {
- mConfig.mCallback.onDialogAnimatedIn(getRequestId());
- mBiometricView.onDialogAnimatedIn();
+ final boolean delayFingerprint = mBiometricView.isCoex() && !mConfig.mRequireConfirmation;
+ mConfig.mCallback.onDialogAnimatedIn(getRequestId(), !delayFingerprint);
+ mBiometricView.onDialogAnimatedIn(!delayFingerprint);
}
}
+ @Override
+ public PromptViewModel getViewModel() {
+ return mPromptViewModel;
+ }
+
@VisibleForTesting
static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) {
final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
@@ -922,26 +938,5 @@
if (mConfig != null) {
pw.println(" config.sensorIds exist=" + (mConfig.mSensorIds != null));
}
- final AuthBiometricView biometricView = mBiometricView;
- pw.println(" scrollView=" + findViewById(R.id.biometric_scrollview));
- pw.println(" biometricView=" + biometricView);
- if (biometricView != null) {
- int[] ids = {
- R.id.title,
- R.id.subtitle,
- R.id.description,
- R.id.biometric_icon_frame,
- R.id.biometric_icon,
- R.id.indicator,
- R.id.button_bar,
- R.id.button_negative,
- R.id.button_use_credential,
- R.id.button_confirm,
- R.id.button_try_again
- };
- for (final int id: ids) {
- pw.println(" " + biometricView.findViewById(id));
- }
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index fd9cee0..57f1928 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -37,7 +37,6 @@
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricManager.Authenticators;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricStateListener;
import android.hardware.biometrics.IBiometricContextListener;
@@ -71,14 +70,18 @@
import com.android.settingslib.udfps.UdfpsOverlayParams;
import com.android.settingslib.udfps.UdfpsUtils;
import com.android.systemui.CoreStartable;
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.data.repository.BiometricType;
import com.android.systemui.statusbar.CommandQueue;
@@ -86,8 +89,6 @@
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.concurrency.Execution;
-import kotlin.Unit;
-
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -101,6 +102,9 @@
import javax.inject.Inject;
import javax.inject.Provider;
+import kotlin.Unit;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the
* appropriate biometric UI (e.g. BiometricDialogView).
@@ -109,7 +113,7 @@
* {@link com.android.keyguard.KeyguardUpdateMonitor}
*/
@SysUISingleton
-public class AuthController implements CoreStartable, CommandQueue.Callbacks,
+public class AuthController implements CoreStartable, CommandQueue.Callbacks,
AuthDialogCallback, DozeReceiver {
private static final String TAG = "AuthController";
@@ -118,6 +122,7 @@
private final Handler mHandler;
private final Context mContext;
+ private final FeatureFlags mFeatureFlags;
private final Execution mExecution;
private final CommandQueue mCommandQueue;
private final ActivityTaskManager mActivityTaskManager;
@@ -125,13 +130,15 @@
@Nullable private final FaceManager mFaceManager;
private final Provider<UdfpsController> mUdfpsControllerFactory;
private final Provider<SideFpsController> mSidefpsControllerFactory;
+ private final CoroutineScope mApplicationCoroutineScope;
// TODO: these should be migrated out once ready
- @NonNull private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor;
-
@NonNull private final Provider<AuthBiometricFingerprintViewModel>
mAuthBiometricFingerprintViewModelProvider;
+ @NonNull private final Provider<PromptCredentialInteractor> mPromptCredentialInteractor;
+ @NonNull private final Provider<PromptSelectorInteractor> mPromptSelectorInteractor;
@NonNull private final Provider<CredentialViewModel> mCredentialViewModelProvider;
+ @NonNull private final Provider<PromptViewModel> mPromptViewModelProvider;
@NonNull private final LogContextInteractor mLogContextInteractor;
private final Display mDisplay;
@@ -461,7 +468,7 @@
}
@Override
- public void onDialogAnimatedIn(long requestId) {
+ public void onDialogAnimatedIn(long requestId, boolean startFingerprintNow) {
final IBiometricSysuiReceiver receiver = getCurrentReceiver(requestId);
if (receiver == null) {
Log.w(TAG, "Skip onDialogAnimatedIn");
@@ -469,7 +476,22 @@
}
try {
- receiver.onDialogAnimatedIn();
+ receiver.onDialogAnimatedIn(startFingerprintNow);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e);
+ }
+ }
+
+ @Override
+ public void onStartFingerprintNow(long requestId) {
+ final IBiometricSysuiReceiver receiver = getCurrentReceiver(requestId);
+ if (receiver == null) {
+ Log.e(TAG, "onStartUdfpsNow: Receiver is null");
+ return;
+ }
+
+ try {
+ receiver.onStartFingerprintNow();
} catch (RemoteException e) {
Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e);
}
@@ -728,6 +750,8 @@
}
@Inject
public AuthController(Context context,
+ @NonNull FeatureFlags featureFlags,
+ @Application CoroutineScope applicationCoroutineScope,
Execution execution,
CommandQueue commandQueue,
ActivityTaskManager activityTaskManager,
@@ -743,16 +767,19 @@
@NonNull LockPatternUtils lockPatternUtils,
@NonNull UdfpsLogger udfpsLogger,
@NonNull LogContextInteractor logContextInteractor,
- @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor,
@NonNull Provider<AuthBiometricFingerprintViewModel>
authBiometricFingerprintViewModelProvider,
+ @NonNull Provider<PromptCredentialInteractor> promptCredentialInteractorProvider,
+ @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
@NonNull Provider<CredentialViewModel> credentialViewModelProvider,
+ @NonNull Provider<PromptViewModel> promptViewModelProvider,
@NonNull InteractionJankMonitor jankMonitor,
@Main Handler handler,
@Background DelayableExecutor bgExecutor,
@NonNull VibratorHelper vibrator,
@NonNull UdfpsUtils udfpsUtils) {
mContext = context;
+ mFeatureFlags = featureFlags;
mExecution = execution;
mUserManager = userManager;
mLockPatternUtils = lockPatternUtils;
@@ -773,10 +800,13 @@
mFaceEnrolledForUser = new SparseBooleanArray();
mVibratorHelper = vibrator;
mUdfpsUtils = udfpsUtils;
+ mApplicationCoroutineScope = applicationCoroutineScope;
mLogContextInteractor = logContextInteractor;
- mBiometricPromptInteractor = biometricPromptInteractor;
mAuthBiometricFingerprintViewModelProvider = authBiometricFingerprintViewModelProvider;
+ mPromptSelectorInteractor = promptSelectorInteractorProvider;
+ mPromptCredentialInteractor = promptCredentialInteractorProvider;
+ mPromptViewModelProvider = promptViewModelProvider;
mCredentialViewModelProvider = credentialViewModelProvider;
mOrientationListener = new BiometricDisplayListener(
@@ -913,8 +943,7 @@
@Override
public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver,
int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
- int userId, long operationId, String opPackageName, long requestId,
- @BiometricMultiSensorMode int multiSensorConfig) {
+ int userId, long operationId, String opPackageName, long requestId) {
@Authenticators.Types final int authenticators = promptInfo.getAuthenticators();
if (DEBUG) {
@@ -927,8 +956,7 @@
+ ", credentialAllowed: " + credentialAllowed
+ ", requireConfirmation: " + requireConfirmation
+ ", operationId: " + operationId
- + ", requestId: " + requestId
- + ", multiSensorConfig: " + multiSensorConfig);
+ + ", requestId: " + requestId);
}
SomeArgs args = SomeArgs.obtain();
args.arg1 = promptInfo;
@@ -940,7 +968,6 @@
args.arg6 = opPackageName;
args.argl1 = operationId;
args.argl2 = requestId;
- args.argi2 = multiSensorConfig;
boolean skipAnimation = false;
if (mCurrentDialog != null) {
@@ -948,7 +975,7 @@
skipAnimation = true;
}
- showDialog(args, skipAnimation, null /* savedState */);
+ showDialog(args, skipAnimation, null /* savedState */, mPromptViewModelProvider.get());
}
/**
@@ -1171,7 +1198,8 @@
return mFpEnrolledForUser.getOrDefault(userId, false);
}
- private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
+ private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState,
+ @Nullable PromptViewModel viewModel) {
mCurrentDialogArgs = args;
final PromptInfo promptInfo = (PromptInfo) args.arg1;
@@ -1182,7 +1210,6 @@
final String opPackageName = (String) args.arg6;
final long operationId = args.argl1;
final long requestId = args.argl2;
- @BiometricMultiSensorMode final int multiSensorConfig = args.argi2;
// Create a new dialog but do not replace the current one yet.
final AuthDialog newDialog = buildDialog(
@@ -1195,11 +1222,11 @@
skipAnimation,
operationId,
requestId,
- multiSensorConfig,
mWakefulnessLifecycle,
mPanelInteractionDetector,
mUserManager,
- mLockPatternUtils);
+ mLockPatternUtils,
+ viewModel);
if (newDialog == null) {
Log.e(TAG, "Unsupported type configuration");
@@ -1253,6 +1280,7 @@
// Save the state of the current dialog (buttons showing, etc)
if (mCurrentDialog != null) {
+ final PromptViewModel viewModel = mCurrentDialog.getViewModel();
final Bundle savedState = new Bundle();
mCurrentDialog.onSaveState(savedState);
mCurrentDialog.dismissWithoutCallback(false /* animate */);
@@ -1271,7 +1299,7 @@
promptInfo.setAuthenticators(Authenticators.DEVICE_CREDENTIAL);
}
- showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState);
+ showDialog(mCurrentDialogArgs, true /* skipAnimation */, savedState, viewModel);
}
}
}
@@ -1286,26 +1314,28 @@
protected AuthDialog buildDialog(@Background DelayableExecutor bgExecutor,
PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds,
String opPackageName, boolean skipIntro, long operationId, long requestId,
- @BiometricMultiSensorMode int multiSensorConfig,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
@NonNull UserManager userManager,
- @NonNull LockPatternUtils lockPatternUtils) {
- return new AuthContainerView.Builder(mContext)
- .setCallback(this)
- .setPromptInfo(promptInfo)
- .setRequireConfirmation(requireConfirmation)
- .setUserId(userId)
- .setOpPackageName(opPackageName)
- .setSkipIntro(skipIntro)
- .setOperationId(operationId)
- .setRequestId(requestId)
- .setMultiSensorConfig(multiSensorConfig)
- .setScaleFactorProvider(() -> getScaleFactor())
- .build(bgExecutor, sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle,
- panelInteractionDetector, userManager, lockPatternUtils,
- mInteractionJankMonitor, mBiometricPromptInteractor,
- mAuthBiometricFingerprintViewModelProvider, mCredentialViewModelProvider);
+ @NonNull LockPatternUtils lockPatternUtils,
+ @NonNull PromptViewModel viewModel) {
+ final AuthContainerView.Config config = new AuthContainerView.Config();
+ config.mContext = mContext;
+ config.mCallback = this;
+ config.mPromptInfo = promptInfo;
+ config.mRequireConfirmation = requireConfirmation;
+ config.mUserId = userId;
+ config.mOpPackageName = opPackageName;
+ config.mSkipIntro = skipIntro;
+ config.mOperationId = operationId;
+ config.mRequestId = requestId;
+ config.mSensorIds = sensorIds;
+ config.mScaleProvider = this::getScaleFactor;
+ return new AuthContainerView(config, mFeatureFlags, mApplicationCoroutineScope, mFpProps, mFaceProps,
+ wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
+ mInteractionJankMonitor, mAuthBiometricFingerprintViewModelProvider,
+ mPromptCredentialInteractor, mPromptSelectorInteractor, viewModel,
+ mCredentialViewModelProvider, bgExecutor);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
index 51f39b3..b6eabfa 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
@@ -24,13 +24,17 @@
import android.view.WindowManager;
import com.android.systemui.Dumpable;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Interface for the biometric dialog UI.
+ *
+ * TODO(b/251476085): remove along with legacy controller once flag is removed
*/
+@Deprecated
public interface AuthDialog extends Dumpable {
String KEY_CONTAINER_GOING_AWAY = "container_going_away";
@@ -70,10 +74,10 @@
* {@link AuthPanelController}.
*/
class LayoutParams {
- final int mMediumHeight;
- final int mMediumWidth;
+ public final int mMediumHeight;
+ public final int mMediumWidth;
- LayoutParams(int mediumWidth, int mediumHeight) {
+ public LayoutParams(int mediumWidth, int mediumHeight) {
mMediumWidth = mediumWidth;
mMediumHeight = mediumHeight;
}
@@ -172,4 +176,6 @@
* must remain fixed on the physical sensor location.
*/
void onOrientationChanged();
+
+ PromptViewModel getViewModel();
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
index bbe461a..9a21940 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java
@@ -70,5 +70,10 @@
/**
* Notifies when the dialog has finished animating.
*/
- void onDialogAnimatedIn(long requestId);
+ void onDialogAnimatedIn(long requestId, boolean startFingerprintNow);
+
+ /**
+ * Notifies that the fingerprint sensor should be started now.
+ */
+ void onStartFingerprintNow(long requestId);
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
index f5f4640..f56bb88 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt
@@ -84,9 +84,6 @@
}
}
- /** If the icon should act as a "retry" button in the [currentState]. */
- fun iconTapSendsRetryWhen(@BiometricState currentState: Int): Boolean = false
-
/** Call during [updateState] if the controller is not [deactivated]. */
abstract fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
index ad10071..acdde34 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
@@ -114,16 +114,7 @@
}
private int getTopBound(@Position int position) {
- switch (position) {
- case POSITION_BOTTOM:
- return Math.max(mContainerHeight - mContentHeight - mMargin, mMargin);
- case POSITION_LEFT:
- case POSITION_RIGHT:
- return Math.max((mContainerHeight - mContentHeight) / 2, mMargin);
- default:
- Log.e(TAG, "Unrecognized position: " + position);
- return getTopBound(POSITION_BOTTOM);
- }
+ return Math.max(mContainerHeight - mContentHeight - mMargin, mMargin);
}
public void setContainerDimensions(int containerWidth, int containerHeight) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
index 43745bf..16dc42a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
@@ -63,7 +63,7 @@
}
@NonNull
- AuthDialog.LayoutParams onMeasureInternal(
+ public AuthDialog.LayoutParams onMeasureInternal(
int width, int height, @NonNull AuthDialog.LayoutParams layoutParams,
float scaleFactor) {
@@ -86,7 +86,7 @@
* too cleanly support this case. So, let's have the onLayout code translate the sensor location
* instead.
*/
- int getBottomSpacerHeight() {
+ public int getBottomSpacerHeight() {
return mBottomSpacerHeight;
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index 096d941..ddf1457 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -31,6 +31,8 @@
import com.android.systemui.biometrics.domain.interactor.LogContextInteractorImpl
import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.util.concurrency.ThreadFactory
import dagger.Binds
@@ -57,6 +59,11 @@
@Binds
@SysUISingleton
+ fun providesPromptSelectorInteractor(impl: PromptSelectorInteractorImpl):
+ PromptSelectorInteractor
+
+ @Binds
+ @SysUISingleton
fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
index 92a13cf..b4dc272 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
@@ -2,7 +2,7 @@
import android.hardware.biometrics.PromptInfo
import com.android.systemui.biometrics.AuthController
-import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
@@ -35,12 +35,20 @@
/** The kind of credential to use (biometric, pin, pattern, etc.). */
val kind: StateFlow<PromptKind>
+ /**
+ * If explicit confirmation is required.
+ *
+ * Note: overlaps/conflicts with [PromptInfo.isConfirmationRequested], which needs clean up.
+ */
+ val isConfirmationRequired: StateFlow<Boolean>
+
/** Update the prompt configuration, which should be set before [isShowing]. */
fun setPrompt(
promptInfo: PromptInfo,
userId: Int,
gatekeeperChallenge: Long?,
- kind: PromptKind = PromptKind.ANY_BIOMETRIC,
+ kind: PromptKind,
+ requireConfirmation: Boolean = false,
)
/** Unset the prompt info. */
@@ -74,29 +82,35 @@
private val _userId: MutableStateFlow<Int?> = MutableStateFlow(null)
override val userId = _userId.asStateFlow()
- private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
+ private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric())
override val kind = _kind.asStateFlow()
+ private val _isConfirmationRequired: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ override val isConfirmationRequired = _isConfirmationRequired.asStateFlow()
+
override fun setPrompt(
promptInfo: PromptInfo,
userId: Int,
gatekeeperChallenge: Long?,
kind: PromptKind,
+ requireConfirmation: Boolean,
) {
_kind.value = kind
_userId.value = userId
_challenge.value = gatekeeperChallenge
_promptInfo.value = promptInfo
+ _isConfirmationRequired.value = requireConfirmation
}
override fun unsetPrompt() {
_promptInfo.value = null
_userId.value = null
_challenge.value = null
- _kind.value = PromptKind.ANY_BIOMETRIC
+ _kind.value = PromptKind.Biometric()
+ _isConfirmationRequired.value = false
}
companion object {
- private const val TAG = "BiometricPromptRepository"
+ private const val TAG = "PromptRepositoryImpl"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
index 6362c2f..d92c217 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
@@ -1,14 +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 com.android.systemui.biometrics.domain.interactor
import android.hardware.biometrics.PromptInfo
import com.android.internal.widget.LockPatternView
import com.android.internal.widget.LockscreenCredential
import com.android.systemui.biometrics.Utils
-import com.android.systemui.biometrics.data.model.PromptKind
import com.android.systemui.biometrics.data.repository.PromptRepository
import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -24,8 +40,16 @@
/**
* Business logic for BiometricPrompt's CredentialViews, which primarily includes checking a users
* PIN, pattern, or password credential instead of a biometric.
+ *
+ * This is used to cache the calling app's options that were given to the underlying authenticate
+ * APIs and should be set before any UI is shown to the user.
+ *
+ * There can be at most one request active at a given time. Use [resetPrompt] when no request is
+ * active to clear the cache.
+ *
+ * Views that use any biometric should use [PromptSelectorInteractor] instead.
*/
-class BiometricPromptCredentialInteractor
+class PromptCredentialInteractor
@Inject
constructor(
@Background private val bgDispatcher: CoroutineDispatcher,
@@ -36,7 +60,7 @@
val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing
/** Metadata about the current credential prompt, including app-supplied preferences. */
- val prompt: Flow<BiometricPromptRequest?> =
+ val prompt: Flow<BiometricPromptRequest.Credential?> =
combine(
biometricPromptRepository.promptInfo,
biometricPromptRepository.challenge,
@@ -48,20 +72,20 @@
}
when (kind) {
- PromptKind.PIN ->
+ PromptKind.Pin ->
BiometricPromptRequest.Credential.Pin(
info = promptInfo,
userInfo = userInfo(userId),
operationInfo = operationInfo(challenge)
)
- PromptKind.PATTERN ->
+ PromptKind.Pattern ->
BiometricPromptRequest.Credential.Pattern(
info = promptInfo,
userInfo = userInfo(userId),
operationInfo = operationInfo(challenge),
stealthMode = credentialInteractor.isStealthModeActive(userId)
)
- PromptKind.PASSWORD ->
+ PromptKind.Password ->
BiometricPromptRequest.Credential.Password(
info = promptInfo,
userInfo = userInfo(userId),
@@ -182,8 +206,8 @@
/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */
private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind =
when (this) {
- Utils.CREDENTIAL_PIN -> PromptKind.PIN
- Utils.CREDENTIAL_PASSWORD -> PromptKind.PASSWORD
- Utils.CREDENTIAL_PATTERN -> PromptKind.PATTERN
- else -> PromptKind.ANY_BIOMETRIC
+ Utils.CREDENTIAL_PIN -> PromptKind.Pin
+ Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
+ Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
+ else -> PromptKind.Biometric()
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
new file mode 100644
index 0000000..e6e07f9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -0,0 +1,186 @@
+/*
+ * 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.domain.interactor
+
+import android.hardware.biometrics.PromptInfo
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.Utils.getCredentialType
+import com.android.systemui.biometrics.Utils.isDeviceCredentialAllowed
+import com.android.systemui.biometrics.data.repository.PromptRepository
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
+import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
+import com.android.systemui.biometrics.domain.model.BiometricUserInfo
+import com.android.systemui.biometrics.shared.model.PromptKind
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/**
+ * Business logic for BiometricPrompt's biometric view variants (face, fingerprint, coex, etc.).
+ *
+ * This is used to cache the calling app's options that were given to the underlying authenticate
+ * APIs and should be set before any UI is shown to the user.
+ *
+ * There can be at most one request active at a given time. Use [resetPrompt] when no request is
+ * active to clear the cache.
+ *
+ * Views that use credential fallback should use [PromptCredentialInteractor] instead.
+ */
+interface PromptSelectorInteractor {
+
+ /** Static metadata about the current prompt. */
+ val prompt: Flow<BiometricPromptRequest.Biometric?>
+
+ /** If using a credential is allowed. */
+ val isCredentialAllowed: Flow<Boolean>
+
+ /**
+ * The kind of credential the user may use as a fallback or [PromptKind.Biometric] if unknown or
+ * not [isCredentialAllowed].
+ */
+ val credentialKind: Flow<PromptKind>
+
+ /** If the API caller requested explicit confirmation after successful authentication. */
+ val isConfirmationRequested: Flow<Boolean>
+
+ /** Use biometrics for authentication. */
+ fun useBiometricsForAuthentication(
+ promptInfo: PromptInfo,
+ requireConfirmation: Boolean,
+ userId: Int,
+ challenge: Long,
+ modalities: BiometricModalities,
+ )
+
+ /** Use credential-based authentication instead of biometrics. */
+ fun useCredentialsForAuthentication(
+ promptInfo: PromptInfo,
+ @Utils.CredentialType kind: Int,
+ userId: Int,
+ challenge: Long,
+ )
+
+ /** Unset the current authentication request. */
+ fun resetPrompt()
+}
+
+@SysUISingleton
+class PromptSelectorInteractorImpl
+@Inject
+constructor(
+ private val promptRepository: PromptRepository,
+ lockPatternUtils: LockPatternUtils,
+) : PromptSelectorInteractor {
+
+ override val prompt: Flow<BiometricPromptRequest.Biometric?> =
+ combine(
+ promptRepository.promptInfo,
+ promptRepository.challenge,
+ promptRepository.userId,
+ promptRepository.kind
+ ) { promptInfo, challenge, userId, kind ->
+ if (promptInfo == null || userId == null || challenge == null) {
+ return@combine null
+ }
+
+ when (kind) {
+ is PromptKind.Biometric ->
+ BiometricPromptRequest.Biometric(
+ info = promptInfo,
+ userInfo = BiometricUserInfo(userId = userId),
+ operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge),
+ modalities = kind.activeModalities,
+ )
+ else -> null
+ }
+ }
+
+ override val isConfirmationRequested: Flow<Boolean> =
+ promptRepository.promptInfo
+ .map { info -> info?.isConfirmationRequested ?: false }
+ .distinctUntilChanged()
+
+ override val isCredentialAllowed: Flow<Boolean> =
+ promptRepository.promptInfo
+ .map { info -> if (info != null) isDeviceCredentialAllowed(info) else false }
+ .distinctUntilChanged()
+
+ override val credentialKind: Flow<PromptKind> =
+ combine(prompt, isCredentialAllowed) { prompt, isAllowed ->
+ if (prompt != null && isAllowed) {
+ when (
+ getCredentialType(lockPatternUtils, prompt.userInfo.deviceCredentialOwnerId)
+ ) {
+ Utils.CREDENTIAL_PIN -> PromptKind.Pin
+ Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
+ Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
+ else -> PromptKind.Biometric()
+ }
+ } else {
+ PromptKind.Biometric()
+ }
+ }
+
+ override fun useBiometricsForAuthentication(
+ promptInfo: PromptInfo,
+ requireConfirmation: Boolean,
+ userId: Int,
+ challenge: Long,
+ modalities: BiometricModalities
+ ) {
+ promptRepository.setPrompt(
+ promptInfo = promptInfo,
+ userId = userId,
+ gatekeeperChallenge = challenge,
+ kind = PromptKind.Biometric(modalities),
+ requireConfirmation = requireConfirmation,
+ )
+ }
+
+ override fun useCredentialsForAuthentication(
+ promptInfo: PromptInfo,
+ @Utils.CredentialType kind: Int,
+ userId: Int,
+ challenge: Long,
+ ) {
+ promptRepository.setPrompt(
+ promptInfo = promptInfo,
+ userId = userId,
+ gatekeeperChallenge = challenge,
+ kind = kind.asBiometricPromptCredential(),
+ )
+ }
+
+ override fun resetPrompt() {
+ promptRepository.unsetPrompt()
+ }
+}
+
+// TODO(b/251476085): remove along with Utils.CredentialType
+/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */
+private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind =
+ when (this) {
+ Utils.CREDENTIAL_PIN -> PromptKind.Pin
+ Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
+ Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
+ else -> PromptKind.Biometric()
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModalities.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModalities.kt
new file mode 100644
index 0000000..274f58a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModalities.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.domain.model
+
+import android.hardware.biometrics.SensorProperties
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+
+/** The available modalities for an operation. */
+data class BiometricModalities(
+ val fingerprintProperties: FingerprintSensorPropertiesInternal? = null,
+ val faceProperties: FaceSensorPropertiesInternal? = null,
+) {
+ /** If there are no available modalities. */
+ val isEmpty: Boolean
+ get() = !hasFingerprint && !hasFace
+
+ /** If fingerprint authentication is available (and [fingerprintProperties] is non-null). */
+ val hasFingerprint: Boolean
+ get() = fingerprintProperties != null
+
+ /** If fingerprint authentication is available (and [faceProperties] is non-null). */
+ val hasFace: Boolean
+ get() = faceProperties != null
+
+ /** If only face authentication is enabled. */
+ val hasFaceOnly: Boolean
+ get() = hasFace && !hasFingerprint
+
+ /** If only fingerprint authentication is enabled. */
+ val hasFingerprintOnly: Boolean
+ get() = hasFingerprint && !hasFace
+
+ /** If face & fingerprint authentication is enabled (coex). */
+ val hasFaceAndFingerprint: Boolean
+ get() = hasFingerprint && hasFace
+
+ /** If [hasFace] and it is configured as a STRONG class 3 biometric. */
+ val isFaceStrong: Boolean
+ get() = faceProperties?.sensorStrength == SensorProperties.STRENGTH_STRONG
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.kt
new file mode 100644
index 0000000..3197c09
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.domain.model
+
+import android.hardware.biometrics.BiometricAuthenticator
+
+/** Shadows [BiometricAuthenticator.Modality] for Kotlin use within SysUI. */
+enum class BiometricModality {
+ None,
+ Fingerprint,
+ Face,
+}
+
+/** Convert a framework [BiometricAuthenticator.Modality] to a SysUI [BiometricModality]. */
+@BiometricAuthenticator.Modality
+fun Int.asBiometricModality(): BiometricModality =
+ when (this) {
+ BiometricAuthenticator.TYPE_FINGERPRINT -> BiometricModality.Fingerprint
+ BiometricAuthenticator.TYPE_FACE -> BiometricModality.Face
+ else -> BiometricModality.None
+ }
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 5ee0381..75de47d 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
@@ -21,6 +21,7 @@
info: PromptInfo,
userInfo: BiometricUserInfo,
operationInfo: BiometricOperationInfo,
+ val modalities: BiometricModalities,
) :
BiometricPromptRequest(
title = info.title?.toString() ?: "",
@@ -28,7 +29,9 @@
description = info.description?.toString() ?: "",
userInfo = userInfo,
operationInfo = operationInfo
- )
+ ) {
+ val negativeButtonText: String = info.negativeButtonText?.toString() ?: ""
+ }
/** Prompt using a credential (pin, pattern, password). */
sealed class Credential(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
similarity index 64%
rename from packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
rename to packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
index e82646f..416fc64 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * 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.
@@ -14,15 +14,19 @@
* limitations under the License.
*/
-package com.android.systemui.biometrics.data.model
+package com.android.systemui.biometrics.shared.model
import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.domain.model.BiometricModalities
// TODO(b/251476085): this should eventually replace Utils.CredentialType
/** Credential options for biometric prompt. Shadows [Utils.CredentialType]. */
-enum class PromptKind {
- ANY_BIOMETRIC,
- PIN,
- PATTERN,
- PASSWORD,
+sealed interface PromptKind {
+ data class Biometric(
+ val activeModalities: BiometricModalities = BiometricModalities(),
+ ) : PromptKind
+
+ object Pin : PromptKind
+ object Pattern : PromptKind
+ object Password : PromptKind
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
new file mode 100644
index 0000000..fb246cd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
@@ -0,0 +1,182 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Insets;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.biometrics.AuthBiometricFingerprintIconController;
+import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.AuthDialog;
+import com.android.systemui.biometrics.UdfpsDialogMeasureAdapter;
+
+import kotlin.Pair;
+
+/**
+ * Contains the Biometric views (title, subtitle, icon, buttons, etc.).
+ *
+ * TODO(b/251476085): get the udfps junk out of here, at a minimum. Likely can be replaced with a
+ * normal LinearLayout.
+ */
+public class BiometricPromptLayout extends LinearLayout {
+
+ private static final String TAG = "BiometricPromptLayout";
+
+ @NonNull
+ private final WindowManager mWindowManager;
+ @Nullable
+ private AuthController.ScaleFactorProvider mScaleFactorProvider;
+ @Nullable
+ private UdfpsDialogMeasureAdapter mUdfpsAdapter;
+
+ private final boolean mUseCustomBpSize;
+ private final int mCustomBpWidth;
+ private final int mCustomBpHeight;
+
+ public BiometricPromptLayout(Context context) {
+ this(context, null);
+ }
+
+ public BiometricPromptLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mWindowManager = context.getSystemService(WindowManager.class);
+
+ mUseCustomBpSize = getResources().getBoolean(R.bool.use_custom_bp_size);
+ mCustomBpWidth = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_width);
+ mCustomBpHeight = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_height);
+ }
+
+ @Deprecated
+ public void setUdfpsAdapter(@NonNull UdfpsDialogMeasureAdapter adapter,
+ @NonNull AuthController.ScaleFactorProvider scaleProvider) {
+ mUdfpsAdapter = adapter;
+ mScaleFactorProvider = scaleProvider != null ? scaleProvider : () -> 1.0f;
+ }
+
+ @Deprecated
+ public boolean isUdfps() {
+ return mUdfpsAdapter != null;
+ }
+
+ @Deprecated
+ public void updateFingerprintAffordanceSize(
+ @NonNull AuthBiometricFingerprintIconController iconController) {
+ if (mUdfpsAdapter != null) {
+ final int sensorDiameter = mUdfpsAdapter.getSensorDiameter(
+ mScaleFactorProvider.provide());
+ iconController.setIconLayoutParamSize(new Pair(sensorDiameter, sensorDiameter));
+ }
+ }
+
+ @NonNull
+ private AuthDialog.LayoutParams onMeasureInternal(int width, int height) {
+ int totalHeight = 0;
+ final int numChildren = getChildCount();
+ for (int i = 0; i < numChildren; i++) {
+ final View child = getChildAt(i);
+
+ if (child.getId() == R.id.space_above_icon
+ || child.getId() == R.id.space_below_icon
+ || 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.biometric_icon_frame) {
+ final View iconView = findViewById(R.id.biometric_icon);
+ child.measure(
+ MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().width,
+ MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height,
+ MeasureSpec.EXACTLY));
+ } else if (child.getId() == R.id.biometric_icon) {
+ child.measure(
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
+ } else {
+ child.measure(
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
+ }
+
+ if (child.getVisibility() != View.GONE) {
+ totalHeight += child.getMeasuredHeight();
+ }
+ }
+
+ final AuthDialog.LayoutParams params = new AuthDialog.LayoutParams(width, totalHeight);
+ if (mUdfpsAdapter != null) {
+ return mUdfpsAdapter.onMeasureInternal(width, height, params,
+ (mScaleFactorProvider != null) ? mScaleFactorProvider.provide() : 1.0f);
+ } else {
+ return params;
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (mUseCustomBpSize) {
+ width = mCustomBpWidth;
+ height = mCustomBpHeight;
+ } else {
+ width = Math.min(width, height);
+ }
+
+ // add nav bar insets since the parent AuthContainerView
+ // uses LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ final Insets insets = mWindowManager.getMaximumWindowMetrics().getWindowInsets()
+ .getInsets(WindowInsets.Type.navigationBars());
+ final AuthDialog.LayoutParams params = onMeasureInternal(width, height);
+ setMeasuredDimension(params.mMediumWidth + insets.left + insets.right,
+ params.mMediumHeight + insets.bottom);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mUdfpsAdapter != null) {
+ // Move the UDFPS icon and indicator text if necessary. This probably only needs to
+ // happen for devices where the UDFPS sensor is too low.
+ // TODO(b/201510778): Update this logic to support cases where the sensor or text
+ // overlap the button bar area.
+ final float bottomSpacerHeight = mUdfpsAdapter.getBottomSpacerHeight();
+ Log.w(TAG, "bottomSpacerHeight: " + bottomSpacerHeight);
+ if (bottomSpacerHeight < 0) {
+ final FrameLayout iconFrame = findViewById(R.id.biometric_icon_frame);
+ iconFrame.setTranslationY(-bottomSpacerHeight);
+ final TextView indicator = findViewById(R.id.indicator);
+ indicator.setTranslationY(-bottomSpacerHeight);
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000..8486c3f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -0,0 +1,622 @@
+/*
+ * 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.animation.Animator
+import android.content.Context
+import android.hardware.biometrics.BiometricAuthenticator
+import android.hardware.biometrics.BiometricConstants
+import android.hardware.biometrics.BiometricPrompt
+import android.hardware.face.FaceManager
+import android.os.Bundle
+import android.text.method.ScrollingMovementMethod
+import android.util.Log
+import android.view.View
+import android.view.accessibility.AccessibilityManager
+import android.widget.Button
+import android.widget.TextView
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.airbnb.lottie.LottieAnimationView
+import com.android.systemui.R
+import com.android.systemui.biometrics.AuthBiometricFaceIconController
+import com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceIconController
+import com.android.systemui.biometrics.AuthBiometricFingerprintIconController
+import com.android.systemui.biometrics.AuthBiometricView
+import com.android.systemui.biometrics.AuthBiometricView.Callback
+import com.android.systemui.biometrics.AuthBiometricViewAdapter
+import com.android.systemui.biometrics.AuthIconController
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.android.systemui.biometrics.domain.model.asBiometricModality
+import com.android.systemui.biometrics.shared.model.PromptKind
+import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode
+import com.android.systemui.biometrics.ui.viewmodel.PromptMessage
+import com.android.systemui.biometrics.ui.viewmodel.PromptSize
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+private const val TAG = "BiometricViewBinder"
+
+/** Top-most view binder for BiometricPrompt views. */
+object BiometricViewBinder {
+
+ /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */
+ @JvmStatic
+ fun bind(
+ view: BiometricPromptLayout,
+ viewModel: PromptViewModel,
+ panelViewController: AuthPanelController,
+ jankListener: BiometricJankListener,
+ backgroundView: View,
+ legacyCallback: Callback,
+ applicationScope: CoroutineScope,
+ ): AuthBiometricViewAdapter {
+ val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
+ fun notifyAccessibilityChanged() {
+ Utils.notifyAccessibilityContentChanged(accessibilityManager, view)
+ }
+
+ val textColorError =
+ view.resources.getColor(R.color.biometric_dialog_error, view.context.theme)
+ val textColorHint =
+ view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme)
+
+ val titleView = view.findViewById<TextView>(R.id.title)
+ val subtitleView = view.findViewById<TextView>(R.id.subtitle)
+ val descriptionView = view.findViewById<TextView>(R.id.description)
+
+ // set selected for marquee
+ titleView.isSelected = true
+ subtitleView.isSelected = true
+ descriptionView.movementMethod = ScrollingMovementMethod()
+
+ val iconViewOverlay = view.findViewById<LottieAnimationView>(R.id.biometric_icon_overlay)
+ val iconView = view.findViewById<LottieAnimationView>(R.id.biometric_icon)
+ val indicatorMessageView = view.findViewById<TextView>(R.id.indicator)
+
+ // Negative-side (left) buttons
+ val negativeButton = view.findViewById<Button>(R.id.button_negative)
+ val cancelButton = view.findViewById<Button>(R.id.button_cancel)
+ val credentialFallbackButton = view.findViewById<Button>(R.id.button_use_credential)
+
+ // Positive-side (right) buttons
+ val confirmationButton = view.findViewById<Button>(R.id.button_confirm)
+ val retryButton = view.findViewById<Button>(R.id.button_try_again)
+
+ // TODO(b/251476085): temporary workaround for the unsafe callbacks & legacy controllers
+ val adapter =
+ Spaghetti(
+ view = view,
+ viewModel = viewModel,
+ applicationContext = view.context.applicationContext,
+ applicationScope = applicationScope,
+ )
+
+ // bind to prompt
+ var boundSize = false
+ view.repeatWhenAttached {
+ // these do not change and need to be set before any size transitions
+ val modalities = viewModel.modalities.first()
+ titleView.text = viewModel.title.first()
+ descriptionView.text = viewModel.description.first()
+ subtitleView.text = viewModel.subtitle.first()
+
+ // set button listeners
+ negativeButton.setOnClickListener {
+ legacyCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE)
+ }
+ cancelButton.setOnClickListener {
+ legacyCallback.onAction(Callback.ACTION_USER_CANCELED)
+ }
+ credentialFallbackButton.setOnClickListener {
+ viewModel.onSwitchToCredential()
+ legacyCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL)
+ }
+ confirmationButton.setOnClickListener { viewModel.confirmAuthenticated() }
+ retryButton.setOnClickListener {
+ viewModel.showAuthenticating(isRetry = true)
+ legacyCallback.onAction(Callback.ACTION_BUTTON_TRY_AGAIN)
+ }
+
+ // TODO(b/251476085): migrate legacy icon controllers and remove
+ var legacyState: Int = viewModel.legacyState.value
+ val iconController =
+ modalities.asIconController(
+ view.context,
+ iconView,
+ iconViewOverlay,
+ )
+ adapter.attach(this, iconController, modalities, legacyCallback)
+ if (iconController is AuthBiometricFingerprintIconController) {
+ view.updateFingerprintAffordanceSize(iconController)
+ }
+ if (iconController is HackyCoexIconController) {
+ iconController.faceMode = !viewModel.isConfirmationRequested.first()
+ }
+
+ // the icon controller must be created before this happens for the legacy
+ // sizing code in BiometricPromptLayout to work correctly. Simplify this
+ // when those are also migrated. (otherwise the icon size may not be set to
+ // a pixel value before the view is measured and WRAP_CONTENT will be incorrectly
+ // used as part of the measure spec)
+ if (!boundSize) {
+ boundSize = true
+ BiometricViewSizeBinder.bind(
+ view = view,
+ viewModel = viewModel,
+ viewsToHideWhenSmall =
+ listOf(
+ titleView,
+ subtitleView,
+ descriptionView,
+ ),
+ viewsToFadeInOnSizeChange =
+ listOf(
+ titleView,
+ subtitleView,
+ descriptionView,
+ indicatorMessageView,
+ negativeButton,
+ cancelButton,
+ retryButton,
+ confirmationButton,
+ credentialFallbackButton,
+ ),
+ panelViewController = panelViewController,
+ jankListener = jankListener,
+ )
+ }
+
+ // TODO(b/251476085): migrate legacy icon controllers and remove
+ // The fingerprint sensor is started by the legacy
+ // AuthContainerView#onDialogAnimatedIn in all cases but the implicit coex flow
+ // (delayed mode). In that case, start it on the first transition to delayed
+ // which will be triggered by any auth failure.
+ lifecycleScope.launch {
+ val oldMode = viewModel.fingerprintStartMode.first()
+ viewModel.fingerprintStartMode.collect { newMode ->
+ // trigger sensor to start
+ if (
+ oldMode == FingerprintStartMode.Pending &&
+ newMode == FingerprintStartMode.Delayed
+ ) {
+ legacyCallback.onAction(Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR)
+ }
+
+ if (newMode.isStarted) {
+ // do wonky switch from implicit to explicit flow
+ (iconController as? HackyCoexIconController)?.faceMode = false
+ viewModel.showAuthenticating(
+ modalities.asDefaultHelpMessage(view.context),
+ )
+ }
+ }
+ }
+
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ // handle background clicks
+ launch {
+ combine(viewModel.isAuthenticated, viewModel.size) { (authenticated, _), size ->
+ when {
+ authenticated -> false
+ size == PromptSize.SMALL -> false
+ size == PromptSize.LARGE -> false
+ else -> true
+ }
+ }
+ .collect { dismissOnClick ->
+ backgroundView.setOnClickListener {
+ if (dismissOnClick) {
+ legacyCallback.onAction(Callback.ACTION_USER_CANCELED)
+ } else {
+ Log.w(TAG, "Ignoring background click")
+ }
+ }
+ }
+ }
+
+ // set messages
+ launch {
+ viewModel.isIndicatorMessageVisible.collect { show ->
+ indicatorMessageView.visibility = show.asVisibleOrHidden()
+ }
+ }
+
+ // configure & hide/disable buttons
+ launch {
+ viewModel.credentialKind
+ .map { kind ->
+ when (kind) {
+ PromptKind.Pin ->
+ view.resources.getString(R.string.biometric_dialog_use_pin)
+ PromptKind.Password ->
+ view.resources.getString(R.string.biometric_dialog_use_password)
+ PromptKind.Pattern ->
+ view.resources.getString(R.string.biometric_dialog_use_pattern)
+ else -> ""
+ }
+ }
+ .collect { credentialFallbackButton.text = it }
+ }
+ launch { viewModel.negativeButtonText.collect { negativeButton.text = it } }
+ launch {
+ viewModel.isConfirmButtonVisible.collect { show ->
+ confirmationButton.visibility = show.asVisibleOrGone()
+ }
+ }
+ launch {
+ viewModel.isCancelButtonVisible.collect { show ->
+ cancelButton.visibility = show.asVisibleOrGone()
+ }
+ }
+ launch {
+ viewModel.isNegativeButtonVisible.collect { show ->
+ negativeButton.visibility = show.asVisibleOrGone()
+ }
+ }
+ launch {
+ viewModel.isTryAgainButtonVisible.collect { show ->
+ retryButton.visibility = show.asVisibleOrGone()
+ }
+ }
+ launch {
+ viewModel.isCredentialButtonVisible.collect { show ->
+ credentialFallbackButton.visibility = show.asVisibleOrGone()
+ }
+ }
+
+ // reuse the icon as a confirm button
+ launch {
+ viewModel.isConfirmButtonVisible
+ .map { isPending ->
+ when {
+ isPending && iconController.actsAsConfirmButton ->
+ View.OnClickListener { viewModel.confirmAuthenticated() }
+ else -> null
+ }
+ }
+ .collect { onClick ->
+ iconViewOverlay.setOnClickListener(onClick)
+ iconView.setOnClickListener(onClick)
+ }
+ }
+
+ // TODO(b/251476085): remove w/ legacy icon controllers
+ // set icon affordance using legacy states
+ // like the old code, this causes animations to repeat on config changes :(
+ // but keep behavior for now as no one has complained...
+ launch {
+ viewModel.legacyState.collect { newState ->
+ iconController.updateState(legacyState, newState)
+ legacyState = newState
+ }
+ }
+
+ // not sure why this is here, but the legacy code did it probably needed?
+ launch {
+ viewModel.isAuthenticating.collect { isAuthenticating ->
+ if (isAuthenticating) {
+ notifyAccessibilityChanged()
+ }
+ }
+ }
+
+ // dismiss prompt when authenticated and confirmed
+ launch {
+ viewModel.isAuthenticated.collect { authState ->
+ if (authState.isAuthenticatedAndConfirmed) {
+ view.announceForAccessibility(
+ view.resources.getString(R.string.biometric_dialog_authenticated)
+ )
+ notifyAccessibilityChanged()
+
+ launch {
+ delay(authState.delay)
+ legacyCallback.onAction(Callback.ACTION_AUTHENTICATED)
+ }
+ }
+ }
+ }
+
+ // show error & help messages
+ launch {
+ viewModel.message.collect { promptMessage ->
+ val isError = promptMessage is PromptMessage.Error
+
+ indicatorMessageView.text = promptMessage.message
+ indicatorMessageView.setTextColor(
+ if (isError) textColorError else textColorHint
+ )
+
+ // select to enable marquee unless a screen reader is enabled
+ // TODO(wenhuiy): this may have recently changed per UX - verify and remove
+ indicatorMessageView.isSelected =
+ !accessibilityManager.isEnabled ||
+ !accessibilityManager.isTouchExplorationEnabled
+
+ notifyAccessibilityChanged()
+ }
+ }
+ }
+ }
+
+ return adapter
+ }
+}
+
+/**
+ * Adapter for legacy events. Remove once legacy controller can be replaced by flagged code.
+ *
+ * These events can be dispatched when the view is being recreated so they need to be delivered to
+ * the view model (which will be retained) via the application scope.
+ *
+ * Do not reference the [view] for anything other than [asView].
+ *
+ * TODO(b/251476085): remove after replacing AuthContainerView
+ */
+private class Spaghetti(
+ private val view: View,
+ private val viewModel: PromptViewModel,
+ private val applicationContext: Context,
+ private val applicationScope: CoroutineScope,
+) : AuthBiometricViewAdapter {
+
+ private var lifecycleScope: CoroutineScope? = null
+ private var modalities: BiometricModalities = BiometricModalities()
+ private var faceFailedAtLeastOnce = false
+ private var legacyCallback: Callback? = null
+
+ override var legacyIconController: AuthIconController? = null
+ private set
+
+ // hacky way to suppress lockout errors
+ private val lockoutErrorStrings =
+ listOf(
+ BiometricConstants.BIOMETRIC_ERROR_LOCKOUT,
+ BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT,
+ )
+ .map { FaceManager.getErrorString(applicationContext, it, 0 /* vendorCode */) }
+
+ fun attach(
+ lifecycleOwner: LifecycleOwner,
+ iconController: AuthIconController,
+ activeModalities: BiometricModalities,
+ callback: Callback,
+ ) {
+ modalities = activeModalities
+ legacyIconController = iconController
+ legacyCallback = callback
+
+ lifecycleOwner.lifecycle.addObserver(
+ object : DefaultLifecycleObserver {
+ override fun onCreate(owner: LifecycleOwner) {
+ lifecycleScope = owner.lifecycleScope
+ iconController.deactivated = false
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ lifecycleScope = null
+ iconController.deactivated = true
+ }
+ }
+ )
+ }
+
+ override fun onDialogAnimatedIn(fingerprintWasStarted: Boolean) {
+ if (fingerprintWasStarted) {
+ viewModel.ensureFingerprintHasStarted(isDelayed = false)
+ viewModel.showAuthenticating(modalities.asDefaultHelpMessage(applicationContext))
+ } else {
+ viewModel.showAuthenticating()
+ }
+ }
+
+ override fun onAuthenticationSucceeded(@BiometricAuthenticator.Modality modality: Int) {
+ applicationScope.launch {
+ val authenticatedModality = modality.asBiometricModality()
+ val msgId = getHelpForSuccessfulAuthentication(authenticatedModality)
+ viewModel.showAuthenticated(
+ modality = authenticatedModality,
+ dismissAfterDelay = 500,
+ helpMessage = if (msgId != null) applicationContext.getString(msgId) else ""
+ )
+ }
+ }
+
+ private suspend fun getHelpForSuccessfulAuthentication(
+ authenticatedModality: BiometricModality,
+ ): Int? =
+ when {
+ // for coex, show a message when face succeeds after fingerprint has also started
+ modalities.hasFaceAndFingerprint &&
+ (viewModel.fingerprintStartMode.first() != FingerprintStartMode.Pending) &&
+ (authenticatedModality == BiometricModality.Face) ->
+ R.string.biometric_dialog_tap_confirm_with_face
+ else -> null
+ }
+
+ override fun onAuthenticationFailed(
+ @BiometricAuthenticator.Modality modality: Int,
+ failureReason: String,
+ ) {
+ val failedModality = modality.asBiometricModality()
+ viewModel.ensureFingerprintHasStarted(isDelayed = true)
+
+ applicationScope.launch {
+ val suppress =
+ modalities.hasFaceAndFingerprint &&
+ (failedModality == BiometricModality.Face) &&
+ faceFailedAtLeastOnce
+ if (failedModality == BiometricModality.Face) {
+ faceFailedAtLeastOnce = true
+ }
+
+ viewModel.showTemporaryError(
+ failureReason,
+ messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
+ authenticateAfterError = modalities.hasFingerprint,
+ suppressIfErrorShowing = suppress,
+ failedModality = failedModality,
+ )
+ }
+ }
+
+ override fun onError(modality: Int, error: String) {
+ val errorModality = modality.asBiometricModality()
+ if (ignoreUnsuccessfulEventsFrom(errorModality, error)) {
+ return
+ }
+
+ applicationScope.launch {
+ val suppress =
+ modalities.hasFaceAndFingerprint && (errorModality == BiometricModality.Face)
+ viewModel.showTemporaryError(
+ error,
+ suppressIfErrorShowing = suppress,
+ )
+ delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
+ legacyCallback?.onAction(Callback.ACTION_ERROR)
+ }
+ }
+
+ override fun onHelp(modality: Int, help: String) {
+ if (ignoreUnsuccessfulEventsFrom(modality.asBiometricModality(), "")) {
+ return
+ }
+
+ applicationScope.launch {
+ viewModel.showTemporaryHelp(
+ help,
+ messageAfterHelp = modalities.asDefaultHelpMessage(applicationContext),
+ )
+ }
+ }
+
+ private fun ignoreUnsuccessfulEventsFrom(modality: BiometricModality, message: String) =
+ when {
+ modalities.hasFaceAndFingerprint ->
+ (modality == BiometricModality.Face) &&
+ !(modalities.isFaceStrong && lockoutErrorStrings.contains(message))
+ else -> false
+ }
+
+ override fun startTransitionToCredentialUI() {
+ applicationScope.launch {
+ viewModel.onSwitchToCredential()
+ legacyCallback?.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL)
+ }
+ }
+
+ override fun requestLayout() {
+ // nothing, for legacy view...
+ }
+
+ override fun restoreState(bundle: Bundle?) {
+ // nothing, for legacy view...
+ }
+
+ override fun onSaveState(bundle: Bundle?) {
+ // nothing, for legacy view...
+ }
+
+ override fun onOrientationChanged() {
+ // nothing, for legacy view...
+ }
+
+ override fun cancelAnimation() {
+ view.animate()?.cancel()
+ }
+
+ override fun isCoex() = modalities.hasFaceAndFingerprint
+
+ override fun asView() = view
+}
+
+private fun BiometricModalities.asDefaultHelpMessage(context: Context): String =
+ when {
+ hasFingerprint -> context.getString(R.string.fingerprint_dialog_touch_sensor)
+ else -> ""
+ }
+
+private fun BiometricModalities.asIconController(
+ context: Context,
+ iconView: LottieAnimationView,
+ iconViewOverlay: LottieAnimationView,
+): AuthIconController =
+ when {
+ hasFaceAndFingerprint -> HackyCoexIconController(context, iconView, iconViewOverlay)
+ hasFingerprint -> AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay)
+ hasFace -> AuthBiometricFaceIconController(context, iconView)
+ else -> throw IllegalStateException("unexpected view type :$this")
+ }
+
+private fun Boolean.asVisibleOrGone(): Int = if (this) View.VISIBLE else View.GONE
+
+private fun Boolean.asVisibleOrHidden(): Int = if (this) View.VISIBLE else View.INVISIBLE
+
+// TODO(b/251476085): proper type?
+typealias BiometricJankListener = Animator.AnimatorListener
+
+// TODO(b/251476085): delete - temporary until the legacy icon controllers are replaced
+private class HackyCoexIconController(
+ context: Context,
+ iconView: LottieAnimationView,
+ iconViewOverlay: LottieAnimationView,
+) : AuthBiometricFingerprintAndFaceIconController(context, iconView, iconViewOverlay) {
+
+ private var state: Int? = null
+ private val faceController = AuthBiometricFaceIconController(context, iconView)
+
+ var faceMode: Boolean = true
+ set(value) {
+ if (field != value) {
+ field = value
+
+ faceController.deactivated = !value
+ iconView.setImageIcon(null)
+ iconViewOverlay.setImageIcon(null)
+ state?.let { updateIcon(AuthBiometricView.STATE_IDLE, it) }
+ }
+ }
+
+ override fun updateIcon(lastState: Int, newState: Int) {
+ if (deactivated) {
+ return
+ }
+
+ if (faceMode) {
+ faceController.updateIcon(lastState, newState)
+ } else {
+ super.updateIcon(lastState, newState)
+ }
+
+ state = newState
+ }
+}
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
new file mode 100644
index 0000000..e4c4e9a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -0,0 +1,209 @@
+/*
+ * 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.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.view.View
+import android.view.ViewGroup
+import android.view.accessibility.AccessibilityManager
+import android.widget.TextView
+import androidx.core.animation.addListener
+import androidx.core.view.doOnLayout
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.R
+import com.android.systemui.biometrics.AuthDialog
+import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.PromptSize
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.biometrics.ui.viewmodel.isLarge
+import com.android.systemui.biometrics.ui.viewmodel.isMedium
+import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall
+import com.android.systemui.biometrics.ui.viewmodel.isSmall
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.launch
+
+/** Helper for [BiometricViewBinder] to handle resize transitions. */
+object BiometricViewSizeBinder {
+
+ /** Resizes [BiometricPromptLayout] and the [panelViewController] via the [PromptViewModel]. */
+ fun bind(
+ view: BiometricPromptLayout,
+ viewModel: PromptViewModel,
+ viewsToHideWhenSmall: List<TextView>,
+ viewsToFadeInOnSizeChange: List<View>,
+ panelViewController: AuthPanelController,
+ jankListener: BiometricJankListener,
+ ) {
+ val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
+ fun notifyAccessibilityChanged() {
+ Utils.notifyAccessibilityContentChanged(accessibilityManager, view)
+ }
+
+ fun startMonitoredAnimation(animators: List<Animator>) {
+ with(AnimatorSet()) {
+ addListener(jankListener)
+ addListener(onEnd = { notifyAccessibilityChanged() })
+ play(animators.first()).apply { animators.drop(1).forEach { next -> with(next) } }
+ start()
+ }
+ }
+
+ val iconHolderView = view.findViewById<View>(R.id.biometric_icon_frame)
+ val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding)
+ val fullSizeYOffset =
+ view.resources.getDimension(R.dimen.biometric_dialog_medium_to_large_translation_offset)
+
+ // cache the original position of the icon view (as done in legacy view)
+ // this must happen before any size changes can be made
+ var iconHolderOriginalY = 0f
+ view.doOnLayout {
+ iconHolderOriginalY = iconHolderView.y
+
+ // bind to prompt
+ // TODO(b/251476085): migrate the legacy panel controller and simplify this
+ view.repeatWhenAttached {
+ var currentSize: PromptSize? = null
+ lifecycleScope.launch {
+ viewModel.size.collect { size ->
+ // prepare for animated size transitions
+ for (v in viewsToHideWhenSmall) {
+ v.showTextOrHide(forceHide = size.isSmall)
+ }
+ if (currentSize == null && size.isSmall) {
+ iconHolderView.alpha = 0f
+ }
+ if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
+ viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
+ }
+
+ // propagate size changes to legacy panel controller and animate transitions
+ view.doOnLayout {
+ val width = view.measuredWidth
+ val height = view.measuredHeight
+
+ when {
+ size.isSmall -> {
+ iconHolderView.alpha = 1f
+ iconHolderView.y =
+ view.height - iconHolderView.height - iconPadding
+ val newHeight =
+ iconHolderView.height + 2 * iconPadding.toInt() -
+ iconHolderView.paddingTop -
+ iconHolderView.paddingBottom
+ panelViewController.updateForContentDimensions(
+ width,
+ newHeight,
+ 0, /* animateDurationMs */
+ )
+ }
+ size.isMedium && currentSize.isSmall -> {
+ val duration = AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS
+ panelViewController.updateForContentDimensions(
+ width,
+ height,
+ duration,
+ )
+ startMonitoredAnimation(
+ listOf(
+ iconHolderView.asVerticalAnimator(
+ duration = duration.toLong(),
+ toY = iconHolderOriginalY,
+ ),
+ viewsToFadeInOnSizeChange.asFadeInAnimator(
+ duration = duration.toLong(),
+ delay = duration.toLong(),
+ ),
+ )
+ )
+ }
+ size.isMedium && currentSize.isNullOrNotSmall -> {
+ panelViewController.updateForContentDimensions(
+ width,
+ height,
+ 0, /* animateDurationMs */
+ )
+ }
+ size.isLarge -> {
+ val duration = AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS
+ panelViewController.setUseFullScreen(true)
+ panelViewController.updateForContentDimensions(
+ panelViewController.containerWidth,
+ panelViewController.containerHeight,
+ duration,
+ )
+
+ startMonitoredAnimation(
+ listOf(
+ view.asVerticalAnimator(
+ duration.toLong() * 2 / 3,
+ toY = view.y - fullSizeYOffset
+ ),
+ listOf(view)
+ .asFadeInAnimator(
+ duration = duration.toLong() / 2,
+ delay = duration.toLong(),
+ ),
+ )
+ )
+ // TODO(b/251476085): clean up (copied from legacy)
+ if (view.isAttachedToWindow) {
+ val parent = view.parent as? ViewGroup
+ parent?.removeView(view)
+ }
+ }
+ }
+
+ currentSize = size
+ notifyAccessibilityChanged()
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun TextView.showTextOrHide(forceHide: Boolean = false) {
+ visibility = if (forceHide || text.isBlank()) View.GONE else View.VISIBLE
+}
+
+private fun View.asVerticalAnimator(
+ duration: Long,
+ toY: Float,
+ fromY: Float = this.y
+): ValueAnimator {
+ val animator = ValueAnimator.ofFloat(fromY, toY)
+ animator.duration = duration
+ animator.addUpdateListener { y = it.animatedValue as Float }
+ return animator
+}
+
+private fun List<View>.asFadeInAnimator(duration: Long, delay: Long): ValueAnimator {
+ forEach { it.alpha = 0f }
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = duration
+ animator.startDelay = delay
+ animator.addUpdateListener {
+ val alpha = it.animatedValue as Float
+ forEach { view -> view.alpha = alpha }
+ }
+ return animator
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
index ba23f1c..a64798c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt
@@ -4,7 +4,7 @@
import android.os.UserHandle
/** View model for the top-level header / info area of BiometricPrompt. */
-interface HeaderViewModel {
+interface CredentialHeaderViewModel {
val user: UserHandle
val title: String
val subtitle: String
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
index 84bbceb..9d7b940 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
@@ -7,8 +7,8 @@
import com.android.internal.widget.LockPatternView
import com.android.systemui.R
import com.android.systemui.biometrics.Utils
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
import com.android.systemui.biometrics.domain.interactor.CredentialStatus
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
@@ -27,11 +27,11 @@
@Inject
constructor(
@Application private val applicationContext: Context,
- private val credentialInteractor: BiometricPromptCredentialInteractor,
+ private val credentialInteractor: PromptCredentialInteractor,
) {
/** Top level information about the prompt. */
- val header: Flow<HeaderViewModel> =
+ val header: Flow<CredentialHeaderViewModel> =
credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>().map {
request ->
BiometricPromptHeaderViewModelImpl(
@@ -109,12 +109,14 @@
}
/** Check a PIN or password and update [validatedAttestation] or [remainingAttempts]. */
- suspend fun checkCredential(text: CharSequence, header: HeaderViewModel) =
+ suspend fun checkCredential(text: CharSequence, header: CredentialHeaderViewModel) =
checkCredential(credentialInteractor.checkCredential(header.asRequest(), text = text))
/** Check a pattern and update [validatedAttestation] or [remainingAttempts]. */
- suspend fun checkCredential(pattern: List<LockPatternView.Cell>, header: HeaderViewModel) =
- checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern))
+ suspend fun checkCredential(
+ pattern: List<LockPatternView.Cell>,
+ header: CredentialHeaderViewModel
+ ) = checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern))
private suspend fun checkCredential(result: CredentialStatus) {
when (result) {
@@ -172,7 +174,7 @@
override val subtitle: String,
override val description: String,
override val icon: Drawable,
-) : HeaderViewModel
+) : CredentialHeaderViewModel
-private fun HeaderViewModel.asRequest(): BiometricPromptRequest.Credential =
+private fun CredentialHeaderViewModel.asRequest(): BiometricPromptRequest.Credential =
(this as BiometricPromptHeaderViewModelImpl).request
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt
new file mode 100644
index 0000000..9cb91b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.biometrics.domain.model.BiometricModality
+
+/**
+ * The authenticated state with the [authenticatedModality] (when [isAuthenticated]) with an
+ * optional [delay] to keep the UI showing before dismissing when [needsUserConfirmation] is not
+ * required.
+ */
+data class PromptAuthState(
+ val isAuthenticated: Boolean,
+ val authenticatedModality: BiometricModality = BiometricModality.None,
+ val needsUserConfirmation: Boolean = false,
+ val delay: Long = 0,
+) {
+ /** If authentication was successful and the user has confirmed (or does not need to). */
+ val isAuthenticatedAndConfirmed: Boolean
+ get() = isAuthenticated && !needsUserConfirmation
+
+ /** If a successful authentication has not occurred. */
+ val isNotAuthenticated: Boolean
+ get() = !isAuthenticated
+
+ /** If a authentication has succeeded and it was done by face (may need confirmation). */
+ val isAuthenticatedByFace: Boolean
+ get() = isAuthenticated && authenticatedModality == BiometricModality.Face
+
+ /** If a authentication has succeeded and it was done by fingerprint (may need confirmation). */
+ val isAuthenticatedByFingerprint: Boolean
+ get() = isAuthenticated && authenticatedModality == BiometricModality.Fingerprint
+
+ /** Copies this state, but toggles [needsUserConfirmation] to false. */
+ fun asConfirmed(): PromptAuthState =
+ PromptAuthState(
+ isAuthenticated = isAuthenticated,
+ authenticatedModality = authenticatedModality,
+ needsUserConfirmation = false,
+ delay = delay,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt
new file mode 100644
index 0000000..219da71
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.viewmodel
+
+/**
+ * A help, hint, or error message to show.
+ *
+ * These typically correspond to the same category of help/error callbacks from the underlying HAL
+ * that runs the biometric operation, but may be customized by the framework.
+ */
+sealed interface PromptMessage {
+
+ /** The message to show the user or the empty string. */
+ val message: String
+ get() =
+ when (this) {
+ is Error -> errorMessage
+ is Help -> helpMessage
+ else -> ""
+ }
+
+ /** If this is an [Error] or [Help] message. */
+ val isErrorOrHelp: Boolean
+ get() = this is Error || this is Help
+
+ /** An error message. */
+ data class Error(val errorMessage: String) : PromptMessage
+
+ /** A help message. */
+ data class Help(val helpMessage: String) : PromptMessage
+
+ /** No message. */
+ object Empty : PromptMessage
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptSize.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptSize.kt
new file mode 100644
index 0000000..d779062
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptSize.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.viewmodel
+
+/** The size of a biometric prompt. */
+enum class PromptSize {
+ /** Minimal UI, showing only biometric icon. */
+ SMALL,
+ /** Normal-sized biometric UI, showing title, icon, buttons, etc. */
+ MEDIUM,
+ /** Full-screen credential UI. */
+ LARGE,
+}
+
+val PromptSize?.isSmall: Boolean
+ get() = this != null && this == PromptSize.SMALL
+
+val PromptSize?.isNotSmall: Boolean
+ get() = this != null && this != PromptSize.SMALL
+
+val PromptSize?.isNullOrNotSmall: Boolean
+ get() = this == null || this != PromptSize.SMALL
+
+val PromptSize?.isMedium: Boolean
+ get() = this != null && this == PromptSize.MEDIUM
+
+val PromptSize?.isLarge: Boolean
+ get() = this != null && this == PromptSize.LARGE
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
new file mode 100644
index 0000000..2f8ed09
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -0,0 +1,453 @@
+/*
+ * 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.viewmodel
+
+import android.hardware.biometrics.BiometricPrompt
+import android.util.Log
+import com.android.systemui.biometrics.AuthBiometricView
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.android.systemui.biometrics.shared.model.PromptKind
+import javax.inject.Inject
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** ViewModel for BiometricPrompt. */
+class PromptViewModel
+@Inject
+constructor(
+ private val interactor: PromptSelectorInteractor,
+) {
+ /** The set of modalities available for this prompt */
+ val modalities: Flow<BiometricModalities> =
+ interactor.prompt.map { it?.modalities ?: BiometricModalities() }.distinctUntilChanged()
+
+ // TODO(b/251476085): remove after icon controllers are migrated - do not keep this state
+ private var _legacyState = MutableStateFlow(AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN)
+ val legacyState: StateFlow<Int> = _legacyState.asStateFlow()
+
+ private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ /** If the user is currently authenticating (i.e. at least one biometric is scanning). */
+ val isAuthenticating: Flow<Boolean> = _isAuthenticating.asStateFlow()
+
+ private val _isAuthenticated: MutableStateFlow<PromptAuthState> =
+ MutableStateFlow(PromptAuthState(false))
+
+ /** If the user has successfully authenticated and confirmed (when explicitly required). */
+ val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow()
+
+ /** If the API caller requested explicit confirmation after successful authentication. */
+ val isConfirmationRequested: Flow<Boolean> = interactor.isConfirmationRequested
+
+ /** The kind of credential the user has. */
+ val credentialKind: Flow<PromptKind> = interactor.credentialKind
+
+ /** The label to use for the cancel button. */
+ val negativeButtonText: Flow<String> = interactor.prompt.map { it?.negativeButtonText ?: "" }
+
+ private val _message: MutableStateFlow<PromptMessage> = MutableStateFlow(PromptMessage.Empty)
+
+ /** A message to show the user, if there is an error, hint, or help to show. */
+ val message: Flow<PromptMessage> = _message.asStateFlow()
+
+ private val isRetrySupported: Flow<Boolean> = modalities.map { it.hasFace }
+
+ private val _fingerprintStartMode = MutableStateFlow(FingerprintStartMode.Pending)
+
+ /** Fingerprint sensor state. */
+ val fingerprintStartMode: Flow<FingerprintStartMode> = _fingerprintStartMode.asStateFlow()
+
+ private val _forceLargeSize = MutableStateFlow(false)
+ private val _forceMediumSize = MutableStateFlow(false)
+
+ /** The size of the prompt. */
+ val size: Flow<PromptSize> =
+ combine(
+ _forceLargeSize,
+ _forceMediumSize,
+ modalities,
+ interactor.isConfirmationRequested,
+ fingerprintStartMode,
+ ) { forceLarge, forceMedium, modalities, confirmationRequired, fpStartMode ->
+ when {
+ forceLarge -> PromptSize.LARGE
+ forceMedium -> PromptSize.MEDIUM
+ modalities.hasFaceOnly && !confirmationRequired -> PromptSize.SMALL
+ modalities.hasFaceAndFingerprint &&
+ !confirmationRequired &&
+ fpStartMode == FingerprintStartMode.Pending -> PromptSize.SMALL
+ else -> PromptSize.MEDIUM
+ }
+ }
+ .distinctUntilChanged()
+
+ /** Title for the prompt. */
+ val title: Flow<String> = interactor.prompt.map { it?.title ?: "" }.distinctUntilChanged()
+
+ /** Subtitle for the prompt. */
+ val subtitle: Flow<String> = interactor.prompt.map { it?.subtitle ?: "" }.distinctUntilChanged()
+
+ /** Description for the prompt. */
+ val description: Flow<String> =
+ interactor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
+
+ /** If the indicator (help, error) message should be shown. */
+ val isIndicatorMessageVisible: Flow<Boolean> =
+ combine(
+ size,
+ message,
+ ) { size, message ->
+ size.isNotSmall && message.message.isNotBlank()
+ }
+ .distinctUntilChanged()
+
+ /** If the auth is pending confirmation and the confirm button should be shown. */
+ val isConfirmButtonVisible: Flow<Boolean> =
+ combine(
+ size,
+ isAuthenticated,
+ ) { size, authState ->
+ size.isNotSmall && authState.isAuthenticated && authState.needsUserConfirmation
+ }
+ .distinctUntilChanged()
+
+ /** If the negative button should be shown. */
+ val isNegativeButtonVisible: Flow<Boolean> =
+ combine(
+ size,
+ isAuthenticated,
+ interactor.isCredentialAllowed,
+ ) { size, authState, credentialAllowed ->
+ size.isNotSmall && authState.isNotAuthenticated && !credentialAllowed
+ }
+ .distinctUntilChanged()
+
+ /** If the cancel button should be shown (. */
+ val isCancelButtonVisible: Flow<Boolean> =
+ combine(
+ size,
+ isAuthenticated,
+ isNegativeButtonVisible,
+ isConfirmButtonVisible,
+ ) { size, authState, showNegativeButton, showConfirmButton ->
+ size.isNotSmall &&
+ authState.isAuthenticated &&
+ !showNegativeButton &&
+ showConfirmButton
+ }
+ .distinctUntilChanged()
+
+ private val _canTryAgainNow = MutableStateFlow(false)
+ /**
+ * If authentication can be manually restarted via the try again button or touching a
+ * fingerprint sensor.
+ */
+ val canTryAgainNow: Flow<Boolean> =
+ combine(
+ _canTryAgainNow,
+ size,
+ isAuthenticated,
+ isRetrySupported,
+ ) { readyToTryAgain, size, authState, supportsRetry ->
+ readyToTryAgain && size.isNotSmall && supportsRetry && authState.isNotAuthenticated
+ }
+ .distinctUntilChanged()
+
+ /** If the try again button show be shown (only the button, see [canTryAgainNow]). */
+ val isTryAgainButtonVisible: Flow<Boolean> =
+ combine(
+ canTryAgainNow,
+ modalities,
+ ) { tryAgainIsPossible, modalities ->
+ tryAgainIsPossible && modalities.hasFaceOnly
+ }
+ .distinctUntilChanged()
+
+ /** If the credential fallback button show be shown. */
+ val isCredentialButtonVisible: Flow<Boolean> =
+ combine(
+ size,
+ isAuthenticated,
+ interactor.isCredentialAllowed,
+ ) { size, authState, credentialAllowed ->
+ size.isNotSmall && authState.isNotAuthenticated && credentialAllowed
+ }
+ .distinctUntilChanged()
+
+ private var messageJob: Job? = null
+
+ /**
+ * Show a temporary error [message] associated with an optional [failedModality].
+ *
+ * An optional [messageAfterError] will be shown via [showAuthenticating] when
+ * [authenticateAfterError] is set (or via [showHelp] when not set) after the error is
+ * dismissed.
+ *
+ * The error is ignored if the user has already authenticated and it is treated as
+ * [onSilentError] if [suppressIfErrorShowing] is set and an error message is already showing.
+ */
+ suspend fun showTemporaryError(
+ message: String,
+ messageAfterError: String = "",
+ authenticateAfterError: Boolean = false,
+ suppressIfErrorShowing: Boolean = false,
+ failedModality: BiometricModality = BiometricModality.None,
+ ) = coroutineScope {
+ if (_isAuthenticated.value.isAuthenticated) {
+ return@coroutineScope
+ }
+ if (_message.value.isErrorOrHelp && suppressIfErrorShowing) {
+ onSilentError(failedModality)
+ return@coroutineScope
+ }
+
+ _isAuthenticating.value = false
+ _isAuthenticated.value = PromptAuthState(false)
+ _forceMediumSize.value = true
+ _canTryAgainNow.value = supportsRetry(failedModality)
+ _message.value = PromptMessage.Error(message)
+ _legacyState.value = AuthBiometricView.STATE_ERROR
+
+ messageJob?.cancel()
+ messageJob = launch {
+ delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
+ if (authenticateAfterError) {
+ showAuthenticating(messageAfterError)
+ } else {
+ showHelp(messageAfterError)
+ }
+ }
+ }
+
+ /**
+ * Call instead of [showTemporaryError] if an error from the HAL should be silently ignored to
+ * enable retry (if the [failedModality] supports retrying).
+ *
+ * Ignored if the user has already authenticated.
+ */
+ private fun onSilentError(failedModality: BiometricModality = BiometricModality.None) {
+ if (_isAuthenticated.value.isNotAuthenticated) {
+ _canTryAgainNow.value = supportsRetry(failedModality)
+ }
+ }
+
+ /**
+ * Call to ensure the fingerprint sensor has started. Either when the dialog is first shown
+ * (most cases) or when it should be enabled after a first error (coex implicit flow).
+ */
+ fun ensureFingerprintHasStarted(isDelayed: Boolean) {
+ if (_fingerprintStartMode.value == FingerprintStartMode.Pending) {
+ _fingerprintStartMode.value =
+ if (isDelayed) FingerprintStartMode.Delayed else FingerprintStartMode.Normal
+ }
+ }
+
+ // enable retry only when face fails (fingerprint runs constantly)
+ private fun supportsRetry(failedModality: BiometricModality) =
+ failedModality == BiometricModality.Face
+
+ /**
+ * Show a persistent help message.
+ *
+ * Will be show even if the user has already authenticated.
+ */
+ suspend fun showHelp(message: String) {
+ val alreadyAuthenticated = _isAuthenticated.value.isAuthenticated
+ if (!alreadyAuthenticated) {
+ _isAuthenticating.value = false
+ _isAuthenticated.value = PromptAuthState(false)
+ }
+
+ _message.value =
+ if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty
+ _forceMediumSize.value = true
+ _legacyState.value =
+ if (alreadyAuthenticated) {
+ AuthBiometricView.STATE_PENDING_CONFIRMATION
+ } else {
+ AuthBiometricView.STATE_HELP
+ }
+
+ messageJob?.cancel()
+ messageJob = null
+ }
+
+ /**
+ * Show a temporary help message and transition back to a fixed message.
+ *
+ * Ignored if the user has already authenticated.
+ */
+ suspend fun showTemporaryHelp(
+ message: String,
+ messageAfterHelp: String = "",
+ ) = coroutineScope {
+ if (_isAuthenticated.value.isAuthenticated) {
+ return@coroutineScope
+ }
+
+ _isAuthenticating.value = false
+ _isAuthenticated.value = PromptAuthState(false)
+ _message.value =
+ if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty
+ _forceMediumSize.value = true
+ _legacyState.value = AuthBiometricView.STATE_HELP
+
+ messageJob?.cancel()
+ messageJob = launch {
+ delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())
+ showAuthenticating(messageAfterHelp)
+ }
+ }
+
+ /** Show the user that biometrics are actively running and set [isAuthenticating]. */
+ fun showAuthenticating(message: String = "", isRetry: Boolean = false) {
+ if (_isAuthenticated.value.isAuthenticated) {
+ // TODO(jbolinger): convert to go/tex-apc?
+ Log.w(TAG, "Cannot show authenticating after authenticated")
+ return
+ }
+
+ _isAuthenticating.value = true
+ _isAuthenticated.value = PromptAuthState(false)
+ _message.value = if (message.isBlank()) PromptMessage.Empty else PromptMessage.Help(message)
+ _legacyState.value = AuthBiometricView.STATE_AUTHENTICATING
+
+ // reset the try again button(s) after the user attempts a retry
+ if (isRetry) {
+ _canTryAgainNow.value = false
+ }
+
+ messageJob?.cancel()
+ messageJob = null
+ }
+
+ /**
+ * Show successfully authentication, set [isAuthenticated], and dismiss the prompt after a
+ * [dismissAfterDelay] or prompt for explicit confirmation (if required).
+ */
+ suspend fun showAuthenticated(
+ modality: BiometricModality,
+ dismissAfterDelay: Long,
+ helpMessage: String = "",
+ ) {
+ if (_isAuthenticated.value.isAuthenticated) {
+ // TODO(jbolinger): convert to go/tex-apc?
+ Log.w(TAG, "Cannot show authenticated after authenticated")
+ return
+ }
+
+ _isAuthenticating.value = false
+ val needsUserConfirmation = needsExplicitConfirmation(modality)
+ _isAuthenticated.value =
+ PromptAuthState(true, modality, needsUserConfirmation, dismissAfterDelay)
+ _message.value = PromptMessage.Empty
+ _legacyState.value =
+ if (needsUserConfirmation) {
+ AuthBiometricView.STATE_PENDING_CONFIRMATION
+ } else {
+ AuthBiometricView.STATE_AUTHENTICATED
+ }
+
+ messageJob?.cancel()
+ messageJob = null
+
+ if (helpMessage.isNotBlank()) {
+ showHelp(helpMessage)
+ }
+ }
+
+ private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean {
+ val availableModalities = modalities.first()
+ val confirmationRequested = interactor.isConfirmationRequested.first()
+
+ if (availableModalities.hasFaceAndFingerprint) {
+ // coex only needs confirmation when face is successful, unless it happens on the
+ // first attempt (i.e. without failure) before fingerprint scanning starts
+ if (modality == BiometricModality.Face) {
+ return (fingerprintStartMode.first() != FingerprintStartMode.Pending) ||
+ confirmationRequested
+ }
+ }
+ if (availableModalities.hasFaceOnly) {
+ return confirmationRequested
+ }
+ // fingerprint only never requires confirmation
+ return false
+ }
+
+ /**
+ * Set the prompt's auth state to authenticated and confirmed.
+ *
+ * This should only be used after [showAuthenticated] when the operation requires explicit user
+ * confirmation.
+ */
+ fun confirmAuthenticated() {
+ val authState = _isAuthenticated.value
+ if (authState.isNotAuthenticated) {
+ "Cannot show authenticated after authenticated"
+ Log.w(TAG, "Cannot confirm authenticated when not authenticated")
+ return
+ }
+
+ _isAuthenticated.value = authState.asConfirmed()
+ _message.value = PromptMessage.Empty
+ _legacyState.value = AuthBiometricView.STATE_AUTHENTICATED
+
+ messageJob?.cancel()
+ messageJob = null
+ }
+
+ /**
+ * Switch to the credential view.
+ *
+ * TODO(b/251476085): this should be decoupled from the shared panel controller
+ */
+ fun onSwitchToCredential() {
+ _forceLargeSize.value = true
+ }
+
+ companion object {
+ private const val TAG = "PromptViewModel"
+ }
+}
+
+/** How the fingerprint sensor was started for the prompt. */
+enum class FingerprintStartMode {
+ /** Fingerprint sensor has not started. */
+ Pending,
+
+ /** Fingerprint sensor started immediately when prompt was displayed. */
+ Normal,
+
+ /** Fingerprint sensor started after the first failure of another passive modality. */
+ Delayed;
+
+ /** If this is [Normal] or [Delayed]. */
+ val isStarted: Boolean
+ get() = this == Normal || this == Delayed
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
index 4c817b2..49a0a3c 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.bouncer.data.repo
+import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
@@ -29,7 +30,15 @@
/** The user-facing message to show in the bouncer. */
val message: StateFlow<String?> = _message.asStateFlow()
+ private val _throttling = MutableStateFlow<AuthenticationThrottledModel?>(null)
+ /** The current authentication throttling state. If `null`, there's no throttling. */
+ val throttling: StateFlow<AuthenticationThrottledModel?> = _throttling.asStateFlow()
+
fun setMessage(message: String?) {
_message.value = message
}
+
+ fun setThrottling(throttling: AuthenticationThrottledModel?) {
+ _throttling.value = throttling
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index 8264fed..e462e2f 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -17,10 +17,12 @@
package com.android.systemui.bouncer.domain.interactor
import android.content.Context
+import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.data.repo.BouncerRepository
+import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
@@ -29,8 +31,11 @@
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/** Encapsulates business logic and application state accessing use-cases. */
@@ -46,7 +51,22 @@
) {
/** The user-facing message to show in the bouncer. */
- val message: StateFlow<String?> = repository.message
+ val message: StateFlow<String?> =
+ combine(
+ repository.message,
+ repository.throttling,
+ ) { message, throttling ->
+ messageOrThrottlingMessage(message, throttling)
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue =
+ messageOrThrottlingMessage(
+ repository.message.value,
+ repository.throttling.value,
+ )
+ )
/**
* The currently-configured authentication method. This determines how the authentication
@@ -55,6 +75,9 @@
val authenticationMethod: StateFlow<AuthenticationMethodModel> =
authenticationInteractor.authenticationMethod
+ /** The current authentication throttling state. If `null`, there's no throttling. */
+ val throttling: StateFlow<AuthenticationThrottledModel?> = repository.throttling
+
init {
applicationScope.launch {
combine(
@@ -129,14 +152,39 @@
fun authenticate(
input: List<Any>,
) {
+ if (repository.throttling.value != null) {
+ return
+ }
+
val isAuthenticated = authenticationInteractor.authenticate(input)
- if (isAuthenticated) {
- sceneInteractor.setCurrentScene(
- containerName = containerName,
- scene = SceneModel(SceneKey.Gone),
- )
- } else {
- repository.setMessage(errorMessage(authenticationMethod.value))
+ val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
+ when {
+ isAuthenticated -> {
+ repository.setThrottling(null)
+ sceneInteractor.setCurrentScene(
+ containerName = containerName,
+ scene = SceneModel(SceneKey.Gone),
+ )
+ }
+ failedAttempts >= THROTTLE_AGGRESSIVELY_AFTER || failedAttempts % THROTTLE_EVERY == 0 ->
+ applicationScope.launch {
+ var remainingDurationSec = THROTTLE_DURATION_SEC
+ while (remainingDurationSec > 0) {
+ repository.setThrottling(
+ AuthenticationThrottledModel(
+ failedAttemptCount = failedAttempts,
+ totalDurationSec = THROTTLE_DURATION_SEC,
+ remainingDurationSec = remainingDurationSec,
+ )
+ )
+ remainingDurationSec--
+ delay(1000)
+ }
+
+ repository.setThrottling(null)
+ clearMessage()
+ }
+ else -> repository.setMessage(errorMessage(authenticationMethod.value))
}
}
@@ -163,10 +211,31 @@
}
}
+ private fun messageOrThrottlingMessage(
+ message: String?,
+ throttling: AuthenticationThrottledModel?,
+ ): String {
+ return when {
+ throttling != null ->
+ applicationContext.getString(
+ com.android.internal.R.string.lockscreen_too_many_failed_attempts_countdown,
+ throttling.remainingDurationSec,
+ )
+ message != null -> message
+ else -> ""
+ }
+ }
+
@AssistedFactory
interface Factory {
fun create(
containerName: String,
): BouncerInteractor
}
+
+ companion object {
+ @VisibleForTesting const val THROTTLE_DURATION_SEC = 30
+ @VisibleForTesting const val THROTTLE_AGGRESSIVELY_AFTER = 15
+ @VisibleForTesting const val THROTTLE_EVERY = 5
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt
new file mode 100644
index 0000000..cbea635
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt
@@ -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 com.android.systemui.bouncer.shared.model
+
+/**
+ * Models application state for when further authentication attempts are being throttled due to too
+ * many consecutive failed authentication attempts.
+ */
+data class AuthenticationThrottledModel(
+ /** Total number of failed attempts so far. */
+ val failedAttemptCount: Int,
+ /** Total amount of time the user has to wait before attempting again. */
+ val totalDurationSec: Int,
+ /** Remaining amount of time the user has to wait before attempting again. */
+ val remainingDurationSec: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index ebefb78..774a559 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -16,4 +16,14 @@
package com.android.systemui.bouncer.ui.viewmodel
-sealed interface AuthMethodBouncerViewModel
+import kotlinx.coroutines.flow.StateFlow
+
+sealed interface AuthMethodBouncerViewModel {
+ /**
+ * Whether user input is enabled.
+ *
+ * If `false`, user input should be completely ignored in the UI as the user is "locked out" of
+ * being able to attempt to unlock the device.
+ */
+ val isInputEnabled: StateFlow<Boolean>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index c6528d0..02991bd 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.bouncer.ui.viewmodel
import android.content.Context
+import com.android.systemui.R
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.dagger.qualifiers.Application
@@ -24,10 +25,14 @@
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
/** Holds UI state and handles user input on bouncer UIs. */
class BouncerViewModel
@@ -40,16 +45,42 @@
) {
private val interactor: BouncerInteractor = interactorFactory.create(containerName)
+ /**
+ * Whether updates to the message should be cross-animated from one message to another.
+ *
+ * If `false`, no animation should be applied, the message text should just be replaced
+ * instantly.
+ */
+ val isMessageUpdateAnimationsEnabled: StateFlow<Boolean> =
+ interactor.throttling
+ .map { it == null }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = interactor.throttling.value == null,
+ )
+
+ private val isInputEnabled: StateFlow<Boolean> =
+ interactor.throttling
+ .map { it == null }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = interactor.throttling.value == null,
+ )
+
private val pin: PinBouncerViewModel by lazy {
PinBouncerViewModel(
applicationScope = applicationScope,
interactor = interactor,
+ isInputEnabled = isInputEnabled,
)
}
private val password: PasswordBouncerViewModel by lazy {
PasswordBouncerViewModel(
interactor = interactor,
+ isInputEnabled = isInputEnabled,
)
}
@@ -58,6 +89,7 @@
applicationContext = applicationContext,
applicationScope = applicationScope,
interactor = interactor,
+ isInputEnabled = isInputEnabled,
)
}
@@ -81,11 +113,59 @@
initialValue = interactor.message.value ?: "",
)
+ private val _throttlingDialogMessage = MutableStateFlow<String?>(null)
+ /**
+ * A message for a throttling dialog to show when the user has attempted the wrong credential
+ * too many times and now must wait a while before attempting again.
+ *
+ * If `null`, no dialog should be shown.
+ *
+ * Once the dialog is shown, the UI should call [onThrottlingDialogDismissed] when the user
+ * dismisses this dialog.
+ */
+ val throttlingDialogMessage: StateFlow<String?> = _throttlingDialogMessage.asStateFlow()
+
+ init {
+ applicationScope.launch {
+ interactor.throttling
+ .map { model ->
+ model?.let {
+ when (interactor.authenticationMethod.value) {
+ is AuthenticationMethodModel.PIN ->
+ R.string.kg_too_many_failed_pin_attempts_dialog_message
+ is AuthenticationMethodModel.Password ->
+ R.string.kg_too_many_failed_password_attempts_dialog_message
+ is AuthenticationMethodModel.Pattern ->
+ R.string.kg_too_many_failed_pattern_attempts_dialog_message
+ else -> null
+ }?.let { stringResourceId ->
+ applicationContext.getString(
+ stringResourceId,
+ model.failedAttemptCount,
+ model.totalDurationSec,
+ )
+ }
+ }
+ }
+ .distinctUntilChanged()
+ .collect { dialogMessageOrNull ->
+ if (dialogMessageOrNull != null) {
+ _throttlingDialogMessage.value = dialogMessageOrNull
+ }
+ }
+ }
+ }
+
/** Notifies that the emergency services button was clicked. */
fun onEmergencyServicesButtonClicked() {
// TODO(b/280877228): implement this
}
+ /** Notifies that a throttling dialog has been dismissed by the user. */
+ fun onThrottlingDialogDismissed() {
+ _throttlingDialogMessage.value = null
+ }
+
private fun toViewModel(
authMethod: AuthenticationMethodModel,
): AuthMethodBouncerViewModel? {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 730d4e8..c38fcaa 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -24,6 +24,7 @@
/** Holds UI state and handles user input for the password bouncer UI. */
class PasswordBouncerViewModel(
private val interactor: BouncerInteractor,
+ override val isInputEnabled: StateFlow<Boolean>,
) : AuthMethodBouncerViewModel {
private val _password = MutableStateFlow("")
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index eb1b457..1b0b38e 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -37,6 +37,7 @@
private val applicationContext: Context,
applicationScope: CoroutineScope,
private val interactor: BouncerInteractor,
+ override val isInputEnabled: StateFlow<Boolean>,
) : AuthMethodBouncerViewModel {
/** The number of columns in the dot grid. */
@@ -63,6 +64,16 @@
/** All dots on the grid. */
val dots: StateFlow<List<PatternDotViewModel>> = _dots.asStateFlow()
+ /** Whether the pattern itself should be rendered visibly. */
+ val isPatternVisible: StateFlow<Boolean> =
+ interactor.authenticationMethod
+ .map { authMethod -> isPatternVisible(authMethod) }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = isPatternVisible(interactor.authenticationMethod.value),
+ )
+
/** Notifies that the UI has been shown to the user. */
fun onShown() {
interactor.resetMessage()
@@ -146,6 +157,10 @@
_selectedDots.value = linkedSetOf()
}
+ private fun isPatternVisible(authMethodModel: AuthenticationMethodModel): Boolean {
+ return (authMethodModel as? AuthenticationMethodModel.Pattern)?.isPatternVisible ?: false
+ }
+
private fun defaultDots(): List<PatternDotViewModel> {
return buildList {
(0 until columnCount).forEach { x ->
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index f9223cb..2a733d9 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -33,6 +33,7 @@
class PinBouncerViewModel(
private val applicationScope: CoroutineScope,
private val interactor: BouncerInteractor,
+ override val isInputEnabled: StateFlow<Boolean>,
) : AuthMethodBouncerViewModel {
private val entered = MutableStateFlow<List<Int>>(emptyList())
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e118fdf..1fb8906 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -104,6 +104,16 @@
val SENSITIVE_REVEAL_ANIM =
unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
+ // TODO(b/280783617): Tracking Bug
+ @Keep
+ @JvmField
+ val BUILDER_EXTRAS_OVERRIDE =
+ sysPropBooleanFlag(
+ 128,
+ "persist.sysui.notification.builder_extras_override",
+ default = false
+ )
+
// 200 - keyguard/lockscreen
// ** Flag retired **
// public static final BooleanFlag KEYGUARD_LAYOUT =
@@ -136,7 +146,7 @@
* the digits when the clock moves.
*/
@JvmField
- val STEP_CLOCK_ANIMATION = unreleasedFlag(212, "step_clock_animation", teamfood = true)
+ val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
/**
* Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
@@ -241,7 +251,7 @@
/** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */
// TODO(b/279794160): Tracking bug.
@JvmField
- val DELAY_BOUNCER = releasedFlag(235, "delay_bouncer")
+ val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer")
/** Migrate the indication area to the new keyguard root view. */
// TODO(b/280067944): Tracking bug.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 54da680..5d3f5f2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -333,15 +333,29 @@
};
private final IKeyguardService.Stub mBinder = new IKeyguardService.Stub() {
+ private static final String TRACK_NAME = "IKeyguardService";
+
+ /**
+ * Helper for tracing the most-recent call on the IKeyguardService interface.
+ * IKeyguardService is oneway, so we are most interested in the order of the calls as they
+ * are received. We use an async track to make it easier to visualize in the trace.
+ * @param name name of the trace section
+ */
+ private static void trace(String name) {
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0);
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, name, 0);
+ }
@Override // Binder interface
public void addStateMonitorCallback(IKeyguardStateCallback callback) {
+ trace("addStateMonitorCallback");
checkPermission();
mKeyguardViewMediator.addStateMonitorCallback(callback);
}
@Override // Binder interface
public void verifyUnlock(IKeyguardExitCallback callback) {
+ trace("verifyUnlock");
Trace.beginSection("KeyguardService.mBinder#verifyUnlock");
checkPermission();
mKeyguardViewMediator.verifyUnlock(callback);
@@ -350,6 +364,7 @@
@Override // Binder interface
public void setOccluded(boolean isOccluded, boolean animate) {
+ trace("setOccluded isOccluded=" + isOccluded + " animate=" + animate);
Log.d(TAG, "setOccluded(" + isOccluded + ")");
Trace.beginSection("KeyguardService.mBinder#setOccluded");
@@ -360,24 +375,28 @@
@Override // Binder interface
public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
+ trace("dismiss message=" + message);
checkPermission();
mKeyguardViewMediator.dismiss(callback, message);
}
@Override // Binder interface
public void onDreamingStarted() {
+ trace("onDreamingStarted");
checkPermission();
mKeyguardViewMediator.onDreamingStarted();
}
@Override // Binder interface
public void onDreamingStopped() {
+ trace("onDreamingStopped");
checkPermission();
mKeyguardViewMediator.onDreamingStopped();
}
@Override // Binder interface
public void onStartedGoingToSleep(@PowerManager.GoToSleepReason int pmSleepReason) {
+ trace("onStartedGoingToSleep pmSleepReason=" + pmSleepReason);
checkPermission();
mKeyguardViewMediator.onStartedGoingToSleep(
WindowManagerPolicyConstants.translateSleepReasonToOffReason(pmSleepReason));
@@ -388,6 +407,8 @@
@Override // Binder interface
public void onFinishedGoingToSleep(
@PowerManager.GoToSleepReason int pmSleepReason, boolean cameraGestureTriggered) {
+ trace("onFinishedGoingToSleep pmSleepReason=" + pmSleepReason
+ + " cameraGestureTriggered=" + cameraGestureTriggered);
checkPermission();
mKeyguardViewMediator.onFinishedGoingToSleep(
WindowManagerPolicyConstants.translateSleepReasonToOffReason(pmSleepReason),
@@ -399,6 +420,8 @@
@Override // Binder interface
public void onStartedWakingUp(
@PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
+ trace("onStartedWakingUp pmWakeReason=" + pmWakeReason
+ + " cameraGestureTriggered=" + cameraGestureTriggered);
Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp");
checkPermission();
mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
@@ -409,6 +432,7 @@
@Override // Binder interface
public void onFinishedWakingUp() {
+ trace("onFinishedWakingUp");
Trace.beginSection("KeyguardService.mBinder#onFinishedWakingUp");
checkPermission();
mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.FINISHED_WAKING_UP);
@@ -417,6 +441,7 @@
@Override // Binder interface
public void onScreenTurningOn(IKeyguardDrawnCallback callback) {
+ trace("onScreenTurningOn");
Trace.beginSection("KeyguardService.mBinder#onScreenTurningOn");
checkPermission();
mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNING_ON,
@@ -451,6 +476,7 @@
@Override // Binder interface
public void onScreenTurnedOn() {
+ trace("onScreenTurnedOn");
Trace.beginSection("KeyguardService.mBinder#onScreenTurnedOn");
checkPermission();
mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNED_ON);
@@ -460,12 +486,14 @@
@Override // Binder interface
public void onScreenTurningOff() {
+ trace("onScreenTurningOff");
checkPermission();
mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNING_OFF);
}
@Override // Binder interface
public void onScreenTurnedOff() {
+ trace("onScreenTurnedOff");
checkPermission();
mKeyguardViewMediator.onScreenTurnedOff();
mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNED_OFF);
@@ -474,12 +502,14 @@
@Override // Binder interface
public void setKeyguardEnabled(boolean enabled) {
+ trace("setKeyguardEnabled enabled" + enabled);
checkPermission();
mKeyguardViewMediator.setKeyguardEnabled(enabled);
}
@Override // Binder interface
public void onSystemReady() {
+ trace("onSystemReady");
Trace.beginSection("KeyguardService.mBinder#onSystemReady");
checkPermission();
mKeyguardViewMediator.onSystemReady();
@@ -488,24 +518,28 @@
@Override // Binder interface
public void doKeyguardTimeout(Bundle options) {
+ trace("doKeyguardTimeout");
checkPermission();
mKeyguardViewMediator.doKeyguardTimeout(options);
}
@Override // Binder interface
public void setSwitchingUser(boolean switching) {
+ trace("setSwitchingUser switching=" + switching);
checkPermission();
mKeyguardViewMediator.setSwitchingUser(switching);
}
@Override // Binder interface
public void setCurrentUser(int userId) {
+ trace("setCurrentUser userId=" + userId);
checkPermission();
mKeyguardViewMediator.setCurrentUser(userId);
}
- @Override
+ @Override // Binder interface
public void onBootCompleted() {
+ trace("onBootCompleted");
checkPermission();
mKeyguardViewMediator.onBootCompleted();
}
@@ -515,28 +549,33 @@
* {@code IRemoteAnimationRunner#onAnimationStart} instead.
*/
@Deprecated
- @Override
+ @Override // Binder interface
public void startKeyguardExitAnimation(long startTime, long fadeoutDuration) {
+ trace("startKeyguardExitAnimation startTime=" + startTime
+ + " fadeoutDuration=" + fadeoutDuration);
Trace.beginSection("KeyguardService.mBinder#startKeyguardExitAnimation");
checkPermission();
mKeyguardViewMediator.startKeyguardExitAnimation(startTime, fadeoutDuration);
Trace.endSection();
}
- @Override
+ @Override // Binder interface
public void onShortPowerPressedGoHome() {
+ trace("onShortPowerPressedGoHome");
checkPermission();
mKeyguardViewMediator.onShortPowerPressedGoHome();
}
- @Override
+ @Override // Binder interface
public void dismissKeyguardToLaunch(Intent intentToLaunch) {
+ trace("dismissKeyguardToLaunch");
checkPermission();
mKeyguardViewMediator.dismissKeyguardToLaunch(intentToLaunch);
}
- @Override
+ @Override // Binder interface
public void onSystemKeyPressed(int keycode) {
+ trace("onSystemKeyPressed keycode=" + keycode);
checkPermission();
mKeyguardViewMediator.onSystemKeyPressed(keycode);
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 5818fd0..e524189 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -38,6 +38,7 @@
import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
+import android.icu.text.SimpleDateFormat;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -99,7 +100,9 @@
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Date;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;
@@ -281,6 +284,8 @@
private LogArray mPredictionLog = new LogArray(MAX_NUM_LOGGED_PREDICTIONS);
private LogArray mGestureLogInsideInsets = new LogArray(MAX_NUM_LOGGED_GESTURES);
private LogArray mGestureLogOutsideInsets = new LogArray(MAX_NUM_LOGGED_GESTURES);
+ private SimpleDateFormat mLogDateFormat = new SimpleDateFormat("HH:mm:ss.SSS", Locale.US);
+ private Date mTmpLogDate = new Date();
private final GestureNavigationSettingsObserver mGestureNavigationSettingsObserver;
@@ -969,11 +974,17 @@
}
// For debugging purposes, only log edge points
+ long curTime = System.currentTimeMillis();
+ mTmpLogDate.setTime(curTime);
+ String curTimeStr = mLogDateFormat.format(mTmpLogDate);
(isWithinInsets ? mGestureLogInsideInsets : mGestureLogOutsideInsets).log(String.format(
- "Gesture [%d,alw=%B,%B,%B,%B,%B,%B,disp=%s,wl=%d,il=%d,wr=%d,ir=%d,excl=%s]",
- System.currentTimeMillis(), isTrackpadMultiFingerSwipe, mAllowGesture,
+ "Gesture [%d [%s],alw=%B, mltf=%B, left=%B, defLeft=%B, backAlw=%B, disbld=%B,"
+ + " qsDisbld=%b, blkdAct=%B, pip=%B,"
+ + " disp=%s, wl=%d, il=%d, wr=%d, ir=%d, excl=%s]",
+ curTime, curTimeStr, mAllowGesture, isTrackpadMultiFingerSwipe,
mIsOnLeftEdge, mDeferSetIsOnLeftEdge, mIsBackGestureAllowed,
- QuickStepContract.isBackGestureDisabled(mSysUiFlags), mDisplaySize,
+ QuickStepContract.isBackGestureDisabled(mSysUiFlags), mDisabledForQuickstep,
+ mGestureBlockingActivityRunning, mIsInPip, mDisplaySize,
mEdgeWidthLeft, mLeftInset, mEdgeWidthRight, mRightInset, mExcludeRegion));
} else if (mAllowGesture || mLogGesture) {
if (!mThresholdCrossed) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index fb4feb8..a532195 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -33,7 +33,6 @@
import android.content.Context;
import android.graphics.drawable.Icon;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.hardware.biometrics.IBiometricContextListener;
import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.hardware.biometrics.PromptInfo;
@@ -317,7 +316,7 @@
IBiometricSysuiReceiver receiver,
int[] sensorIds, boolean credentialAllowed,
boolean requireConfirmation, int userId, long operationId, String opPackageName,
- long requestId, @BiometricMultiSensorMode int multiSensorConfig) {
+ long requestId) {
}
/** @see IStatusBar#onBiometricAuthenticated(int) */
@@ -956,8 +955,7 @@
@Override
public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver,
int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
- int userId, long operationId, String opPackageName, long requestId,
- @BiometricMultiSensorMode int multiSensorConfig) {
+ int userId, long operationId, String opPackageName, long requestId) {
synchronized (mLock) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = promptInfo;
@@ -969,7 +967,6 @@
args.arg6 = opPackageName;
args.argl1 = operationId;
args.argl2 = requestId;
- args.argi2 = multiSensorConfig;
mHandler.obtainMessage(MSG_BIOMETRIC_SHOW, args)
.sendToTarget();
}
@@ -1573,8 +1570,7 @@
someArgs.argi1 /* userId */,
someArgs.argl1 /* operationId */,
(String) someArgs.arg6 /* opPackageName */,
- someArgs.argl2 /* requestId */,
- someArgs.argi2 /* multiSensorConfig */);
+ someArgs.argl2 /* requestId */);
}
someArgs.recycle();
break;
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 5ba02fa..5bd965c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -73,7 +73,6 @@
import android.os.SystemClock;
import android.os.Trace;
import android.os.VibrationEffect;
-import android.provider.DeviceConfig;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.text.InputFilter;
@@ -113,7 +112,6 @@
import com.android.app.animation.Interpolators;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.graphics.drawable.BackgroundBlurDrawable;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.view.RotationPolicy;
@@ -133,15 +131,11 @@
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.util.AlphaTintDrawableWrapper;
-import com.android.systemui.util.DeviceConfigProxy;
import com.android.systemui.util.RoundedCornerProgressDrawable;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
@@ -198,9 +192,6 @@
private ViewGroup mDialogRowsView;
private ViewGroup mRinger;
- private DeviceConfigProxy mDeviceConfigProxy;
- private Executor mExecutor;
-
/**
* Container for the top part of the dialog, which contains the ringer, the ringer drawer, the
* volume rows, and the ellipsis button. This does not include the live caption button.
@@ -290,14 +281,12 @@
private BackgroundBlurDrawable mDialogRowsViewBackground;
private final InteractionJankMonitor mInteractionJankMonitor;
- private boolean mSeparateNotification;
-
private int mWindowGravity;
@VisibleForTesting
- int mVolumeRingerIconDrawableId;
+ final int mVolumeRingerIconDrawableId = R.drawable.ic_speaker_on;
@VisibleForTesting
- int mVolumeRingerMuteIconDrawableId;
+ final int mVolumeRingerMuteIconDrawableId = R.drawable.ic_speaker_mute;
private int mOriginalGravity;
private final DevicePostureController.Callback mDevicePostureControllerCallback;
@@ -315,8 +304,6 @@
VolumePanelFactory volumePanelFactory,
ActivityStarter activityStarter,
InteractionJankMonitor interactionJankMonitor,
- DeviceConfigProxy deviceConfigProxy,
- Executor executor,
CsdWarningDialog.Factory csdWarningDialogFactory,
DevicePostureController devicePostureController,
Looper looper,
@@ -374,12 +361,6 @@
} else {
mDevicePostureControllerCallback = null;
}
-
- mDeviceConfigProxy = deviceConfigProxy;
- mExecutor = executor;
- mSeparateNotification = mDeviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false);
- updateRingerModeIconSet();
}
/**
@@ -401,44 +382,6 @@
return mWindowGravity;
}
- /**
- * If ringer and notification are the same stream (T and earlier), use notification-like bell
- * icon set.
- * If ringer and notification are separated, then use generic speaker icons.
- */
- private void updateRingerModeIconSet() {
- if (mSeparateNotification) {
- mVolumeRingerIconDrawableId = R.drawable.ic_speaker_on;
- mVolumeRingerMuteIconDrawableId = R.drawable.ic_speaker_mute;
- } else {
- mVolumeRingerIconDrawableId = R.drawable.ic_volume_ringer;
- mVolumeRingerMuteIconDrawableId = R.drawable.ic_volume_ringer_mute;
- }
-
- if (mRingerDrawerMuteIcon != null) {
- mRingerDrawerMuteIcon.setImageResource(mVolumeRingerMuteIconDrawableId);
- }
- if (mRingerDrawerNormalIcon != null) {
- mRingerDrawerNormalIcon.setImageResource(mVolumeRingerIconDrawableId);
- }
- }
-
- /**
- * Change icon for ring stream (not ringer mode icon)
- */
- private void updateRingRowIcon() {
- Optional<VolumeRow> volumeRow = mRows.stream().filter(row -> row.stream == STREAM_RING)
- .findFirst();
- if (volumeRow.isPresent()) {
- VolumeRow volRow = volumeRow.get();
- volRow.iconRes = mSeparateNotification ? R.drawable.ic_ring_volume
- : R.drawable.ic_volume_ringer;
- volRow.iconMuteRes = mSeparateNotification ? R.drawable.ic_ring_volume_off
- : R.drawable.ic_volume_ringer_mute;
- volRow.setIcon(volRow.iconRes, mContext.getTheme());
- }
- }
-
@Override
public void onUiModeChanged() {
mContext.getTheme().applyStyle(mContext.getThemeResId(), true);
@@ -454,9 +397,6 @@
mConfigurationController.addCallback(this);
- mDeviceConfigProxy.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
- mExecutor, this::onDeviceConfigChange);
-
if (mDevicePostureController != null) {
mDevicePostureController.addCallback(mDevicePostureControllerCallback);
}
@@ -467,28 +407,11 @@
mController.removeCallback(mControllerCallbackH);
mHandler.removeCallbacksAndMessages(null);
mConfigurationController.removeCallback(this);
- mDeviceConfigProxy.removeOnPropertiesChangedListener(this::onDeviceConfigChange);
if (mDevicePostureController != null) {
mDevicePostureController.removeCallback(mDevicePostureControllerCallback);
}
}
- /**
- * Update ringer mode icon based on the config
- */
- private void onDeviceConfigChange(DeviceConfig.Properties properties) {
- Set<String> changeSet = properties.getKeyset();
- if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) {
- boolean newVal = properties.getBoolean(
- SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false);
- if (newVal != mSeparateNotification) {
- mSeparateNotification = newVal;
- updateRingerModeIconSet();
- updateRingRowIcon();
- }
- }
- }
-
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo internalInsetsInfo) {
// Set touchable region insets on the root dialog view. This tells WindowManager that
@@ -699,7 +622,12 @@
mRingerDrawerNormalIcon = mDialog.findViewById(R.id.volume_drawer_normal_icon);
mRingerDrawerNewSelectionBg = mDialog.findViewById(R.id.volume_drawer_selection_background);
- updateRingerModeIconSet();
+ if (mRingerDrawerMuteIcon != null) {
+ mRingerDrawerMuteIcon.setImageResource(mVolumeRingerMuteIconDrawableId);
+ }
+ if (mRingerDrawerNormalIcon != null) {
+ mRingerDrawerNormalIcon.setImageResource(mVolumeRingerIconDrawableId);
+ }
setupRingerDrawer();
@@ -724,13 +652,10 @@
addRow(AudioManager.STREAM_MUSIC,
R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true, true);
if (!AudioSystem.isSingleVolume(mContext)) {
- if (mSeparateNotification) {
- addRow(AudioManager.STREAM_RING, R.drawable.ic_ring_volume,
- R.drawable.ic_ring_volume_off, true, false);
- } else {
- addRow(AudioManager.STREAM_RING, R.drawable.ic_volume_ringer,
- R.drawable.ic_volume_ringer, true, false);
- }
+
+ addRow(AudioManager.STREAM_RING, R.drawable.ic_ring_volume,
+ R.drawable.ic_ring_volume_off, true, false);
+
addRow(STREAM_ALARM,
R.drawable.ic_alarm, R.drawable.ic_volume_alarm_mute, true, false);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index bb04f82..aa4ee54 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -21,7 +21,6 @@
import android.os.Looper;
import com.android.internal.jank.InteractionJankMonitor;
-import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.ActivityStarter;
@@ -31,7 +30,6 @@
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.util.DeviceConfigProxy;
import com.android.systemui.volume.CsdWarningDialog;
import com.android.systemui.volume.VolumeComponent;
import com.android.systemui.volume.VolumeDialogComponent;
@@ -42,8 +40,6 @@
import dagger.Module;
import dagger.Provides;
-import java.util.concurrent.Executor;
-
/** Dagger Module for code in the volume package. */
@Module
public interface VolumeModule {
@@ -63,8 +59,6 @@
VolumePanelFactory volumePanelFactory,
ActivityStarter activityStarter,
InteractionJankMonitor interactionJankMonitor,
- DeviceConfigProxy deviceConfigProxy,
- @Main Executor executor,
CsdWarningDialog.Factory csdFactory,
DevicePostureController devicePostureController,
DumpManager dumpManager) {
@@ -78,8 +72,6 @@
volumePanelFactory,
activityStarter,
interactionJankMonitor,
- deviceConfigProxy,
- executor,
csdFactory,
devicePostureController,
Looper.getMainLooper(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 44c9905..1990c8f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -145,50 +145,59 @@
@Test
fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
assertThat(isUnlocked).isTrue()
+ assertThat(failedAttemptCount).isEqualTo(0)
}
@Test
fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
assertThat(isUnlocked).isFalse()
+ assertThat(failedAttemptCount).isEqualTo(1)
}
@Test
fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate("password".toList())).isTrue()
assertThat(isUnlocked).isTrue()
+ assertThat(failedAttemptCount).isEqualTo(0)
}
@Test
fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate("alohomora".toList())).isFalse()
assertThat(isUnlocked).isFalse()
+ assertThat(failedAttemptCount).isEqualTo(1)
}
@Test
fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(
AuthenticationMethodModel.Pattern(
@@ -230,11 +239,13 @@
)
.isTrue()
assertThat(isUnlocked).isTrue()
+ assertThat(failedAttemptCount).isEqualTo(0)
}
@Test
fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
+ val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts)
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(
AuthenticationMethodModel.Pattern(
@@ -276,6 +287,7 @@
)
.isFalse()
assertThat(isUnlocked).isFalse()
+ assertThat(failedAttemptCount).isEqualTo(1)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt
index 213dc87..2d1e8a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceViewTest.kt
@@ -73,7 +73,7 @@
@Test
fun fingerprintSuccessDoesNotRequireExplicitConfirmation() {
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT)
TestableLooper.get(this).moveTimeForward(1000)
waitForIdleSync()
@@ -84,7 +84,7 @@
@Test
fun faceSuccessRequiresExplicitConfirmation() {
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.onAuthenticationSucceeded(TYPE_FACE)
waitForIdleSync()
@@ -104,7 +104,7 @@
@Test
fun ignoresFaceErrors_faceIsNotClass3_notLockoutError() {
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.onError(TYPE_FACE, "not a face")
waitForIdleSync()
@@ -121,7 +121,7 @@
@Test
fun doNotIgnoresFaceErrors_faceIsClass3_notLockoutError() {
biometricView.isFaceClass3 = true
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.onError(TYPE_FACE, "not a face")
waitForIdleSync()
@@ -138,7 +138,7 @@
@Test
fun doNotIgnoresFaceErrors_faceIsClass3_lockoutError() {
biometricView.isFaceClass3 = true
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.onError(
TYPE_FACE,
FaceManager.getErrorString(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
index 22ebc7e..8e5d96b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintViewTest.kt
@@ -120,7 +120,7 @@
@Test
fun testNegativeButton_beforeAuthentication_sendsActionButtonNegative() {
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
biometricView.mNegativeButton.performClick()
TestableLooper.get(this).moveTimeForward(1000)
waitForIdleSync()
@@ -212,7 +212,7 @@
@Test
fun testIgnoresUselessHelp() {
biometricView.mAnimationDurationHideDialog = 10_000
- biometricView.onDialogAnimatedIn()
+ biometricView.onDialogAnimatedIn(fingerprintWasStarted = true)
waitForIdleSync()
assertThat(biometricView.isAuthenticating).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 9d68cf3..d31a86a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -41,11 +41,15 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakePromptRepository
import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
-import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
+import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
@@ -53,29 +57,34 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
import org.junit.After
+import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
+import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidJUnit4::class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
-class AuthContainerViewTest : SysuiTestCase() {
+open class AuthContainerViewTest : SysuiTestCase() {
@JvmField @Rule
var mockitoRule = MockitoJUnit.rule()
+ private val featureFlags = FakeFeatureFlags()
+
@Mock
lateinit var callback: AuthDialogCallback
@Mock
@@ -91,16 +100,25 @@
@Mock
lateinit var interactionJankMonitor: InteractionJankMonitor
+ // TODO(b/278622168): remove with flag
+ open val useNewBiometricPrompt = false
+
private val testScope = TestScope(StandardTestDispatcher())
private val fakeExecutor = FakeExecutor(FakeSystemClock())
private val biometricPromptRepository = FakePromptRepository()
private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
private val credentialInteractor = FakeCredentialInteractor()
- private val bpCredentialInteractor = BiometricPromptCredentialInteractor(
+ private val bpCredentialInteractor = PromptCredentialInteractor(
Dispatchers.Main.immediate,
biometricPromptRepository,
- credentialInteractor
+ credentialInteractor,
)
+ private val promptSelectorInteractor by lazy {
+ PromptSelectorInteractorImpl(
+ biometricPromptRepository,
+ lockPatternUtils,
+ )
+ }
private val displayStateInteractor = DisplayStateInteractorImpl(
testScope.backgroundScope,
mContext,
@@ -115,6 +133,11 @@
private var authContainer: TestAuthContainerView? = null
+ @Before
+ fun setup() {
+ featureFlags.set(Flags.BIOMETRIC_BP_STRONG, useNewBiometricPrompt)
+ }
+
@After
fun tearDown() {
if (authContainer?.isAttachedToWindow == true) {
@@ -125,7 +148,7 @@
@Test
fun testNotifiesAnimatedIn() {
initializeFingerprintContainer()
- verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+ verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
}
@Test
@@ -164,13 +187,13 @@
container.dismissFromSystemServer()
waitForIdleSync()
- verify(callback, never()).onDialogAnimatedIn(anyLong())
+ verify(callback, never()).onDialogAnimatedIn(anyLong(), anyBoolean())
container.addToView()
waitForIdleSync()
// attaching the view resets the state and allows this to happen again
- verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+ verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
}
@Test
@@ -185,7 +208,7 @@
// the first time is triggered by initializeFingerprintContainer()
// the second time was triggered by dismissWithoutCallback()
- verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+ verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L, true /* startFingerprintNow */)
}
@Test
@@ -479,6 +502,8 @@
this.authenticators = authenticators
}
},
+ featureFlags,
+ testScope.backgroundScope,
fingerprintProps,
faceProps,
wakefulnessLifecycle,
@@ -486,8 +511,10 @@
userManager,
lockPatternUtils,
interactionJankMonitor,
- { bpCredentialInteractor },
{ authBiometricFingerprintViewModel },
+ { promptSelectorInteractor },
+ { bpCredentialInteractor },
+ PromptViewModel(promptSelectorInteractor),
{ credentialViewModel },
Handler(TestableLooper.get(this).looper),
fakeExecutor
@@ -497,7 +524,10 @@
}
}
- override fun waitForIdleSync() = TestableLooper.get(this).processAllMessages()
+ override fun waitForIdleSync() {
+ testScope.runCurrent()
+ TestableLooper.get(this).processAllMessages()
+ }
private fun AuthContainerView.addToView() {
ViewUtils.attachView(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest2.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest2.kt
new file mode 100644
index 0000000..b56d055
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest2.kt
@@ -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 com.android.systemui.biometrics
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.runner.RunWith
+
+// TODO(b/278622168): remove with flag
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class AuthContainerViewTest2 : AuthContainerViewTest() {
+ override val useNewBiometricPrompt = true
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index a326cc7..b9f92a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -18,7 +18,6 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.Authenticators;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
import static com.google.common.truth.Truth.assertThat;
@@ -54,7 +53,6 @@
import android.graphics.Point;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
-import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricStateListener;
import android.hardware.biometrics.ComponentInfoInternal;
@@ -91,10 +89,14 @@
import com.android.settingslib.udfps.UdfpsUtils;
import com.android.systemui.RoboPilotTest;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
import com.android.systemui.biometrics.ui.viewmodel.AuthBiometricFingerprintViewModel;
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
+import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.VibratorHelper;
@@ -171,12 +173,16 @@
@Mock
private InteractionJankMonitor mInteractionJankMonitor;
@Mock
- private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor;
+ private PromptCredentialInteractor mBiometricPromptCredentialInteractor;
+ @Mock
+ private PromptSelectorInteractor mPromptSelectionInteractor;
@Mock
private AuthBiometricFingerprintViewModel mAuthBiometricFingerprintViewModel;
@Mock
private CredentialViewModel mCredentialViewModel;
@Mock
+ private PromptViewModel mPromptViewModel;
+ @Mock
private UdfpsUtils mUdfpsUtils;
@Captor
@@ -194,12 +200,17 @@
private Handler mHandler;
private DelayableExecutor mBackgroundExecutor;
private TestableAuthController mAuthController;
+ private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock
private VibratorHelper mVibratorHelper;
@Before
public void setup() throws RemoteException {
+ // TODO(b/278622168): remove with flag
+ // AuthController simply passes this through to AuthContainerView (does not impact test)
+ mFeatureFlags.set(Flags.BIOMETRIC_BP_STRONG, false);
+
mContextSpy = spy(mContext);
mExecution = new FakeExecution();
mTestableLooper = TestableLooper.get(this);
@@ -952,8 +963,7 @@
0 /* userId */,
0 /* operationId */,
"testPackage",
- REQUEST_ID,
- BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE);
+ REQUEST_ID);
}
private void switchTask(String packageName) {
@@ -993,25 +1003,26 @@
private PromptInfo mLastBiometricPromptInfo;
TestableAuthController(Context context) {
- super(context, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager,
+ super(context, mFeatureFlags, null /* applicationCoroutineScope */,
+ mExecution, mCommandQueue, mActivityTaskManager, mWindowManager,
mFingerprintManager, mFaceManager, () -> mUdfpsController,
() -> mSideFpsController, mDisplayManager, mWakefulnessLifecycle,
mPanelInteractionDetector, mUserManager, mLockPatternUtils, mUdfpsLogger,
- mLogContextInteractor, () -> mBiometricPromptCredentialInteractor,
- () -> mAuthBiometricFingerprintViewModel, () -> mCredentialViewModel,
- mInteractionJankMonitor, mHandler, mBackgroundExecutor, mVibratorHelper,
- mUdfpsUtils);
+ mLogContextInteractor, () -> mAuthBiometricFingerprintViewModel,
+ () -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor,
+ () -> mCredentialViewModel, () -> mPromptViewModel,
+ mInteractionJankMonitor, mHandler,
+ mBackgroundExecutor, mVibratorHelper, mUdfpsUtils);
}
@Override
protected AuthDialog buildDialog(DelayableExecutor bgExecutor, PromptInfo promptInfo,
boolean requireConfirmation, int userId, int[] sensorIds,
String opPackageName, boolean skipIntro, long operationId, long requestId,
- @BiometricManager.BiometricMultiSensorMode int multiSensorConfig,
WakefulnessLifecycle wakefulnessLifecycle,
AuthDialogPanelInteractionDetector panelInteractionDetector,
UserManager userManager,
- LockPatternUtils lockPatternUtils) {
+ LockPatternUtils lockPatternUtils, PromptViewModel viewModel) {
mLastBiometricPromptInfo = promptInfo;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 1379a0e..94244cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -18,10 +18,11 @@
import android.annotation.IdRes
import android.content.Context
-import android.hardware.biometrics.BiometricManager
+import android.hardware.biometrics.BiometricManager.Authenticators
import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.PromptInfo
import android.hardware.biometrics.SensorProperties
+import android.hardware.biometrics.SensorPropertiesInternal
import android.hardware.face.FaceSensorProperties
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorProperties
@@ -61,9 +62,9 @@
private fun buildPromptInfo(allowDeviceCredential: Boolean): PromptInfo {
val promptInfo = PromptInfo()
promptInfo.title = "Title"
- var authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
+ var authenticators = Authenticators.BIOMETRIC_WEAK
if (allowDeviceCredential) {
- authenticators = authenticators or BiometricManager.Authenticators.DEVICE_CREDENTIAL
+ authenticators = authenticators or Authenticators.DEVICE_CREDENTIAL
} else {
promptInfo.negativeButtonText = "Negative"
}
@@ -80,7 +81,8 @@
/** Create [FingerprintSensorPropertiesInternal] for a test. */
internal fun fingerprintSensorPropertiesInternal(
- ids: List<Int> = listOf(0)
+ ids: List<Int> = listOf(0),
+ strong: Boolean = true,
): List<FingerprintSensorPropertiesInternal> {
val componentInfo =
listOf(
@@ -102,7 +104,7 @@
return ids.map { id ->
FingerprintSensorPropertiesInternal(
id,
- SensorProperties.STRENGTH_STRONG,
+ if (strong) SensorProperties.STRENGTH_STRONG else SensorProperties.STRENGTH_WEAK,
5 /* maxEnrollmentsPerUser */,
componentInfo,
FingerprintSensorProperties.TYPE_REAR,
@@ -113,7 +115,8 @@
/** Create [FaceSensorPropertiesInternal] for a test. */
internal fun faceSensorPropertiesInternal(
- ids: List<Int> = listOf(1)
+ ids: List<Int> = listOf(1),
+ strong: Boolean = true,
): List<FaceSensorPropertiesInternal> {
val componentInfo =
listOf(
@@ -135,7 +138,7 @@
return ids.map { id ->
FaceSensorPropertiesInternal(
id,
- SensorProperties.STRENGTH_STRONG,
+ if (strong) SensorProperties.STRENGTH_STRONG else SensorProperties.STRENGTH_WEAK,
2 /* maxEnrollmentsPerUser */,
componentInfo,
FaceSensorProperties.TYPE_RGB,
@@ -146,6 +149,24 @@
}
}
+@Authenticators.Types
+internal fun Collection<SensorPropertiesInternal?>.extractAuthenticatorTypes(): Int {
+ var authenticators = Authenticators.EMPTY_SET
+ mapNotNull { it?.sensorStrength }
+ .forEach { strength ->
+ authenticators =
+ authenticators or
+ when (strength) {
+ SensorProperties.STRENGTH_CONVENIENCE ->
+ Authenticators.BIOMETRIC_CONVENIENCE
+ SensorProperties.STRENGTH_WEAK -> Authenticators.BIOMETRIC_WEAK
+ SensorProperties.STRENGTH_STRONG -> Authenticators.BIOMETRIC_STRONG
+ else -> Authenticators.EMPTY_SET
+ }
+ }
+ return authenticators
+}
+
internal fun promptInfo(
title: String = "title",
subtitle: String = "sub",
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
index 2d5614c..4836af6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt
@@ -4,7 +4,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController
-import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
@@ -60,7 +60,7 @@
@Test
fun setsAndUnsetsPrompt() = runBlockingTest {
- val kind = PromptKind.PIN
+ val kind = PromptKind.Pin
val uid = 8
val challenge = 90L
val promptInfo = PromptInfo()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
index dbcbf41..720a35c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
@@ -9,15 +9,17 @@
import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
import com.android.systemui.biometrics.domain.model.BiometricUserInfo
import com.android.systemui.biometrics.promptInfo
+import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
@@ -36,42 +38,39 @@
@JvmField @Rule var mockitoRule = MockitoJUnit.rule()
- private val dispatcher = UnconfinedTestDispatcher()
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
private val biometricPromptRepository = FakePromptRepository()
private val credentialInteractor = FakeCredentialInteractor()
- private lateinit var interactor: BiometricPromptCredentialInteractor
+ private lateinit var interactor: PromptCredentialInteractor
@Before
fun setup() {
interactor =
- BiometricPromptCredentialInteractor(
- dispatcher,
+ PromptCredentialInteractor(
+ testDispatcher,
biometricPromptRepository,
- credentialInteractor
+ credentialInteractor,
)
}
@Test
fun testIsShowing() =
- runTest(dispatcher) {
- var showing = false
- val job = launch { interactor.isShowing.collect { showing = it } }
+ testScope.runTest {
+ val showing by collectLastValue(interactor.isShowing)
biometricPromptRepository.setIsShowing(false)
assertThat(showing).isFalse()
biometricPromptRepository.setIsShowing(true)
assertThat(showing).isTrue()
-
- job.cancel()
}
@Test
fun testShowError() =
- runTest(dispatcher) {
- var error: CredentialStatus.Fail? = null
- val job = launch { interactor.verificationError.collect { error = it } }
+ testScope.runTest {
+ val error by collectLastValue(interactor.verificationError)
for (msg in listOf("once", "again")) {
interactor.setVerificationError(error(msg))
@@ -80,19 +79,14 @@
interactor.resetVerificationError()
assertThat(error).isNull()
-
- job.cancel()
}
@Test
fun nullWhenNoPromptInfo() =
- runTest(dispatcher) {
- var prompt: BiometricPromptRequest? = null
- val job = launch { interactor.prompt.collect { prompt = it } }
+ testScope.runTest {
+ val prompt by collectLastValue(interactor.prompt)
assertThat(prompt).isNull()
-
- job.cancel()
}
@Test fun usePinCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PIN)
@@ -102,12 +96,11 @@
@Test fun usePatternCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PATTERN)
private fun useCredentialForPrompt(kind: Int) =
- runTest(dispatcher) {
+ testScope.runTest {
val isStealth = false
credentialInteractor.stealthMode = isStealth
- var prompt: BiometricPromptRequest? = null
- val job = launch { interactor.prompt.collect { prompt = it } }
+ val prompt by collectLastValue(interactor.prompt)
val title = "what a prompt"
val subtitle = "s"
@@ -124,14 +117,12 @@
challenge = OPERATION_ID
)
- val p = prompt as? BiometricPromptRequest.Credential
- assertThat(p).isNotNull()
- assertThat(p!!.title).isEqualTo(title)
- assertThat(p.subtitle).isEqualTo(subtitle)
- assertThat(p.description).isEqualTo(description)
- assertThat(p.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
- assertThat(p.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
- assertThat(p)
+ assertThat(prompt?.title).isEqualTo(title)
+ assertThat(prompt?.subtitle).isEqualTo(subtitle)
+ assertThat(prompt?.description).isEqualTo(description)
+ assertThat(prompt?.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
+ assertThat(prompt?.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+ assertThat(prompt)
.isInstanceOf(
when (kind) {
Utils.CREDENTIAL_PIN -> BiometricPromptRequest.Credential.Pin::class.java
@@ -142,25 +133,25 @@
else -> throw Exception("wrong kind")
}
)
- if (p is BiometricPromptRequest.Credential.Pattern) {
- assertThat(p.stealthMode).isEqualTo(isStealth)
+ val pattern = prompt as? BiometricPromptRequest.Credential.Pattern
+ if (pattern != null) {
+ assertThat(pattern.stealthMode).isEqualTo(isStealth)
}
interactor.resetPrompt()
assertThat(prompt).isNull()
-
- job.cancel()
}
@Test
fun checkCredential() =
- runTest(dispatcher) {
+ testScope.runTest {
val hat = ByteArray(4)
credentialInteractor.verifyCredentialResponse = { _ -> flowOf(verified(hat)) }
val errors = mutableListOf<CredentialStatus.Fail?>()
val job = launch { interactor.verificationError.toList(errors) }
+ runCurrent()
val checked =
interactor.checkCredential(pinRequest(), text = "1234")
@@ -168,6 +159,8 @@
assertThat(checked).isNotNull()
assertThat(checked!!.hat).isSameInstanceAs(hat)
+
+ runCurrent()
assertThat(errors.map { it?.error }).containsExactly(null)
job.cancel()
@@ -175,7 +168,7 @@
@Test
fun checkCredentialWhenBad() =
- runTest(dispatcher) {
+ testScope.runTest {
val errorMessage = "bad"
val remainingAttempts = 12
credentialInteractor.verifyCredentialResponse = { _ ->
@@ -184,6 +177,7 @@
val errors = mutableListOf<CredentialStatus.Fail?>()
val job = launch { interactor.verificationError.toList(errors) }
+ runCurrent()
val checked =
interactor.checkCredential(pinRequest(), text = "1234")
@@ -192,6 +186,8 @@
assertThat(checked).isNotNull()
assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts)
assertThat(checked.urgentMessage).isNull()
+
+ runCurrent()
assertThat(errors.map { it?.error }).containsExactly(null, errorMessage).inOrder()
job.cancel()
@@ -199,7 +195,7 @@
@Test
fun checkCredentialWhenBadAndUrgentMessage() =
- runTest(dispatcher) {
+ testScope.runTest {
val error = "not so bad"
val urgentMessage = "really bad"
credentialInteractor.verifyCredentialResponse = { _ ->
@@ -208,6 +204,7 @@
val errors = mutableListOf<CredentialStatus.Fail?>()
val job = launch { interactor.verificationError.toList(errors) }
+ runCurrent()
val checked =
interactor.checkCredential(pinRequest(), text = "1234")
@@ -215,6 +212,8 @@
assertThat(checked).isNotNull()
assertThat(checked!!.urgentMessage).isEqualTo(urgentMessage)
+
+ runCurrent()
assertThat(errors.map { it?.error }).containsExactly(null, error).inOrder()
assertThat(errors.last() as? CredentialStatus.Fail.Error)
.isEqualTo(error(error, 10, urgentMessage))
@@ -224,7 +223,7 @@
@Test
fun checkCredentialWhenBadAndThrottled() =
- runTest(dispatcher) {
+ testScope.runTest {
val remainingAttempts = 3
val error = ":("
val urgentMessage = ":D"
@@ -239,6 +238,7 @@
}
val errors = mutableListOf<CredentialStatus.Fail?>()
val job = launch { interactor.verificationError.toList(errors) }
+ runCurrent()
val checked =
interactor.checkCredential(pinRequest(), text = "1234")
@@ -246,6 +246,8 @@
assertThat(checked).isNotNull()
assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts)
+
+ runCurrent()
assertThat(checked.urgentMessage).isEqualTo(urgentMessage)
assertThat(errors.map { it?.error })
.containsExactly(null, "1", "2", "3", error)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
new file mode 100644
index 0000000..a62ea3b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.hardware.biometrics.BiometricManager.Authenticators
+import android.hardware.biometrics.PromptInfo
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.Utils
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.faceSensorPropertiesInternal
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
+import com.android.systemui.biometrics.shared.model.PromptKind
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+
+private const val TITLE = "hey there"
+private const val SUBTITLE = "ok"
+private const val DESCRIPTION = "football"
+private const val NEGATIVE_TEXT = "escape"
+
+private const val USER_ID = 8
+private const val CHALLENGE = 999L
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptSelectorInteractorImplTest : SysuiTestCase() {
+
+ @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var lockPatternUtils: LockPatternUtils
+
+ private val testScope = TestScope()
+ private val promptRepository = FakePromptRepository()
+
+ private lateinit var interactor: PromptSelectorInteractor
+
+ @Before
+ fun setup() {
+ interactor = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
+ }
+
+ @Test
+ fun useBiometricsAndReset() =
+ testScope.runTest { useBiometricsAndReset(allowCredentialFallback = true) }
+
+ @Test
+ fun useBiometricsAndResetWithoutFallback() =
+ testScope.runTest { useBiometricsAndReset(allowCredentialFallback = false) }
+
+ private fun TestScope.useBiometricsAndReset(allowCredentialFallback: Boolean) {
+ setUserCredentialType(isPassword = true)
+
+ val confirmationRequired = true
+ val info =
+ PromptInfo().apply {
+ title = TITLE
+ subtitle = SUBTITLE
+ description = DESCRIPTION
+ negativeButtonText = NEGATIVE_TEXT
+ isConfirmationRequested = confirmationRequired
+ authenticators =
+ if (allowCredentialFallback) {
+ Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL
+ } else {
+ Authenticators.BIOMETRIC_STRONG
+ }
+ isDeviceCredentialAllowed = allowCredentialFallback
+ }
+ val modalities =
+ BiometricModalities(
+ fingerprintProperties = fingerprintSensorPropertiesInternal().first(),
+ faceProperties = faceSensorPropertiesInternal().first(),
+ )
+
+ val currentPrompt by collectLastValue(interactor.prompt)
+ val credentialKind by collectLastValue(interactor.credentialKind)
+ val isCredentialAllowed by collectLastValue(interactor.isCredentialAllowed)
+ val isExplicitConfirmationRequired by collectLastValue(interactor.isConfirmationRequested)
+
+ assertThat(currentPrompt).isNull()
+
+ interactor.useBiometricsForAuthentication(
+ info,
+ confirmationRequired,
+ USER_ID,
+ CHALLENGE,
+ modalities
+ )
+
+ assertThat(currentPrompt).isNotNull()
+ assertThat(currentPrompt?.title).isEqualTo(TITLE)
+ assertThat(currentPrompt?.description).isEqualTo(DESCRIPTION)
+ assertThat(currentPrompt?.subtitle).isEqualTo(SUBTITLE)
+ assertThat(currentPrompt?.negativeButtonText).isEqualTo(NEGATIVE_TEXT)
+
+ if (allowCredentialFallback) {
+ assertThat(credentialKind).isSameInstanceAs(PromptKind.Password)
+ assertThat(isCredentialAllowed).isTrue()
+ } else {
+ assertThat(credentialKind).isEqualTo(PromptKind.Biometric())
+ assertThat(isCredentialAllowed).isFalse()
+ }
+ assertThat(isExplicitConfirmationRequired).isEqualTo(confirmationRequired)
+
+ interactor.resetPrompt()
+ verifyUnset()
+ }
+
+ @Test
+ fun usePinCredentialAndReset() =
+ testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PIN) }
+
+ @Test
+ fun usePattermCredentialAndReset() =
+ testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PATTERN) }
+
+ @Test
+ fun usePasswordCredentialAndReset() =
+ testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PASSWORD) }
+
+ private fun TestScope.useCredentialAndReset(@Utils.CredentialType kind: Int) {
+ setUserCredentialType(
+ isPin = kind == Utils.CREDENTIAL_PIN,
+ isPassword = kind == Utils.CREDENTIAL_PASSWORD,
+ )
+
+ val info =
+ PromptInfo().apply {
+ title = TITLE
+ subtitle = SUBTITLE
+ description = DESCRIPTION
+ negativeButtonText = NEGATIVE_TEXT
+ authenticators = Authenticators.DEVICE_CREDENTIAL
+ isDeviceCredentialAllowed = true
+ }
+
+ val currentPrompt by collectLastValue(interactor.prompt)
+ val credentialKind by collectLastValue(interactor.credentialKind)
+
+ assertThat(currentPrompt).isNull()
+
+ interactor.useCredentialsForAuthentication(info, kind, USER_ID, CHALLENGE)
+
+ // not using biometrics, should be null with no fallback option
+ assertThat(currentPrompt).isNull()
+ assertThat(credentialKind).isEqualTo(PromptKind.Biometric())
+
+ interactor.resetPrompt()
+ verifyUnset()
+ }
+
+ private fun TestScope.verifyUnset() {
+ val currentPrompt by collectLastValue(interactor.prompt)
+ val credentialKind by collectLastValue(interactor.credentialKind)
+
+ assertThat(currentPrompt).isNull()
+
+ val kind = credentialKind as? PromptKind.Biometric
+ assertThat(kind).isNotNull()
+ assertThat(kind?.activeModalities?.isEmpty).isTrue()
+ }
+
+ private fun setUserCredentialType(isPin: Boolean = false, isPassword: Boolean = false) {
+ whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(any()))
+ .thenReturn(
+ when {
+ isPin -> DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
+ isPassword -> DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
+ else -> DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
+ }
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricModalitiesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricModalitiesTest.kt
new file mode 100644
index 0000000..526b833
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricModalitiesTest.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.domain.model
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.faceSensorPropertiesInternal
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BiometricModalitiesTest : SysuiTestCase() {
+
+ @Test
+ fun isEmpty() {
+ assertThat(BiometricModalities().isEmpty).isTrue()
+ }
+
+ @Test
+ fun fingerprintOnly() {
+ with(
+ BiometricModalities(
+ fingerprintProperties = fingerprintSensorPropertiesInternal().first(),
+ )
+ ) {
+ assertThat(isEmpty).isFalse()
+ assertThat(hasFace).isFalse()
+ assertThat(hasFaceOnly).isFalse()
+ assertThat(hasFingerprint).isTrue()
+ assertThat(hasFingerprintOnly).isTrue()
+ assertThat(hasFaceAndFingerprint).isFalse()
+ }
+ }
+
+ @Test
+ fun faceOnly() {
+ with(BiometricModalities(faceProperties = faceSensorPropertiesInternal().first())) {
+ assertThat(isEmpty).isFalse()
+ assertThat(hasFace).isTrue()
+ assertThat(hasFaceOnly).isTrue()
+ assertThat(hasFingerprint).isFalse()
+ assertThat(hasFingerprintOnly).isFalse()
+ assertThat(hasFaceAndFingerprint).isFalse()
+ }
+ }
+
+ @Test
+ fun faceStrength() {
+ with(
+ BiometricModalities(
+ fingerprintProperties = fingerprintSensorPropertiesInternal(strong = false).first(),
+ faceProperties = faceSensorPropertiesInternal(strong = true).first()
+ )
+ ) {
+ assertThat(isFaceStrong).isTrue()
+ }
+
+ with(
+ BiometricModalities(
+ fingerprintProperties = fingerprintSensorPropertiesInternal(strong = false).first(),
+ faceProperties = faceSensorPropertiesInternal(strong = false).first()
+ )
+ ) {
+ assertThat(isFaceStrong).isFalse()
+ }
+ }
+
+ @Test
+ fun faceAndFingerprint() {
+ with(
+ BiometricModalities(
+ fingerprintProperties = fingerprintSensorPropertiesInternal().first(),
+ faceProperties = faceSensorPropertiesInternal().first(),
+ )
+ ) {
+ assertThat(isEmpty).isFalse()
+ assertThat(hasFace).isTrue()
+ assertThat(hasFingerprint).isTrue()
+ assertThat(hasFaceOnly).isFalse()
+ assertThat(hasFingerprintOnly).isFalse()
+ assertThat(hasFaceAndFingerprint).isTrue()
+ }
+ }
+}
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 4c5e3c1..e352905 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
@@ -2,6 +2,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
import com.android.systemui.biometrics.promptInfo
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -21,11 +22,13 @@
val subtitle = "a"
val description = "request"
+ val fpPros = fingerprintSensorPropertiesInternal().first()
val request =
BiometricPromptRequest.Biometric(
promptInfo(title = title, subtitle = subtitle, description = description),
BiometricUserInfo(USER_ID),
- BiometricOperationInfo(OPERATION_ID)
+ BiometricOperationInfo(OPERATION_ID),
+ BiometricModalities(fingerprintProperties = fpPros),
)
assertThat(request.title).isEqualTo(title)
@@ -33,6 +36,8 @@
assertThat(request.description).isEqualTo(description)
assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID))
assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID))
+ assertThat(request.modalities)
+ .isEqualTo(BiometricModalities(fingerprintProperties = fpPros))
}
@Test
@@ -51,19 +56,19 @@
description = description,
credentialTitle = null,
credentialSubtitle = null,
- credentialDescription = null
+ credentialDescription = null,
),
BiometricUserInfo(USER_ID),
- BiometricOperationInfo(OPERATION_ID)
+ BiometricOperationInfo(OPERATION_ID),
),
BiometricPromptRequest.Credential.Password(
promptInfo(
credentialTitle = title,
credentialSubtitle = subtitle,
- credentialDescription = description
+ credentialDescription = description,
),
BiometricUserInfo(USER_ID),
- BiometricOperationInfo(OPERATION_ID)
+ BiometricOperationInfo(OPERATION_ID),
),
BiometricPromptRequest.Credential.Pattern(
promptInfo(
@@ -71,11 +76,11 @@
description = description,
credentialTitle = title,
credentialSubtitle = null,
- credentialDescription = null
+ credentialDescription = null,
),
BiometricUserInfo(USER_ID),
BiometricOperationInfo(OPERATION_ID),
- stealth
+ stealth,
)
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
index d73cdfc..3245020 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
@@ -2,12 +2,12 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.model.PromptKind
import com.android.systemui.biometrics.data.repository.FakePromptRepository
-import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor
import com.android.systemui.biometrics.domain.interactor.CredentialStatus
import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
import com.android.systemui.biometrics.promptInfo
+import com.android.systemui.biometrics.shared.model.PromptKind
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
@@ -40,17 +40,13 @@
viewModel =
CredentialViewModel(
mContext,
- BiometricPromptCredentialInteractor(
- dispatcher,
- promptRepository,
- credentialInteractor
- )
+ PromptCredentialInteractor(dispatcher, promptRepository, credentialInteractor)
)
}
- @Test fun setsPinInputFlags() = setsInputFlags(PromptKind.PIN, expectFlags = true)
- @Test fun setsPasswordInputFlags() = setsInputFlags(PromptKind.PASSWORD, expectFlags = false)
- @Test fun setsPatternInputFlags() = setsInputFlags(PromptKind.PATTERN, expectFlags = false)
+ @Test fun setsPinInputFlags() = setsInputFlags(PromptKind.Pin, expectFlags = true)
+ @Test fun setsPasswordInputFlags() = setsInputFlags(PromptKind.Password, expectFlags = false)
+ @Test fun setsPatternInputFlags() = setsInputFlags(PromptKind.Pattern, expectFlags = false)
private fun setsInputFlags(type: PromptKind, expectFlags: Boolean) =
runTestWithKind(type) {
@@ -65,10 +61,10 @@
job.cancel()
}
- @Test fun isStealthIgnoredByPin() = isStealthMode(PromptKind.PIN, expectStealth = false)
+ @Test fun isStealthIgnoredByPin() = isStealthMode(PromptKind.Pin, expectStealth = false)
@Test
- fun isStealthIgnoredByPassword() = isStealthMode(PromptKind.PASSWORD, expectStealth = false)
- @Test fun isStealthUsedByPattern() = isStealthMode(PromptKind.PATTERN, expectStealth = true)
+ fun isStealthIgnoredByPassword() = isStealthMode(PromptKind.Password, expectStealth = false)
+ @Test fun isStealthUsedByPattern() = isStealthMode(PromptKind.Pattern, expectStealth = true)
private fun isStealthMode(type: PromptKind, expectStealth: Boolean) =
runTestWithKind(type, init = { credentialInteractor.stealthMode = true }) {
@@ -119,7 +115,7 @@
val attestations = mutableListOf<ByteArray?>()
val remainingAttempts = mutableListOf<RemainingAttempts?>()
- var header: HeaderViewModel? = null
+ var header: CredentialHeaderViewModel? = null
val job = launch {
launch { viewModel.validatedAttestation.toList(attestations) }
launch { viewModel.remainingAttempts.toList(remainingAttempts) }
@@ -147,7 +143,7 @@
val attestations = mutableListOf<ByteArray?>()
val remainingAttempts = mutableListOf<RemainingAttempts?>()
- var header: HeaderViewModel? = null
+ var header: CredentialHeaderViewModel? = null
val job = launch {
launch { viewModel.validatedAttestation.toList(attestations) }
launch { viewModel.remainingAttempts.toList(remainingAttempts) }
@@ -169,7 +165,7 @@
}
private fun runTestWithKind(
- kind: PromptKind = PromptKind.PIN,
+ kind: PromptKind = PromptKind.Pin,
init: () -> Unit = {},
block: suspend TestScope.() -> Unit,
) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt
new file mode 100644
index 0000000..689bb00
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptAuthStateTest : SysuiTestCase() {
+
+ @Test
+ fun notAuthenticated() {
+ with(PromptAuthState(isAuthenticated = false)) {
+ assertThat(isNotAuthenticated).isTrue()
+ assertThat(isAuthenticatedAndConfirmed).isFalse()
+ assertThat(isAuthenticatedByFace).isFalse()
+ assertThat(isAuthenticatedByFingerprint).isFalse()
+ }
+ }
+
+ @Test
+ fun authenticatedByUnknown() {
+ with(PromptAuthState(isAuthenticated = true)) {
+ assertThat(isNotAuthenticated).isFalse()
+ assertThat(isAuthenticatedAndConfirmed).isTrue()
+ assertThat(isAuthenticatedByFace).isFalse()
+ assertThat(isAuthenticatedByFingerprint).isFalse()
+ }
+
+ with(PromptAuthState(isAuthenticated = true, needsUserConfirmation = true)) {
+ assertThat(isNotAuthenticated).isFalse()
+ assertThat(isAuthenticatedAndConfirmed).isFalse()
+ assertThat(isAuthenticatedByFace).isFalse()
+ assertThat(isAuthenticatedByFingerprint).isFalse()
+
+ assertThat(asConfirmed().isAuthenticatedAndConfirmed).isTrue()
+ }
+ }
+
+ @Test
+ fun authenticatedWithFace() {
+ with(
+ PromptAuthState(isAuthenticated = true, authenticatedModality = BiometricModality.Face)
+ ) {
+ assertThat(isNotAuthenticated).isFalse()
+ assertThat(isAuthenticatedAndConfirmed).isTrue()
+ assertThat(isAuthenticatedByFace).isTrue()
+ assertThat(isAuthenticatedByFingerprint).isFalse()
+ }
+ }
+
+ @Test
+ fun authenticatedWithFingerprint() {
+ with(
+ PromptAuthState(
+ isAuthenticated = true,
+ authenticatedModality = BiometricModality.Fingerprint,
+ )
+ ) {
+ assertThat(isNotAuthenticated).isFalse()
+ assertThat(isAuthenticatedAndConfirmed).isTrue()
+ assertThat(isAuthenticatedByFace).isFalse()
+ assertThat(isAuthenticatedByFingerprint).isTrue()
+ }
+ }
+}
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
new file mode 100644
index 0000000..3ba6004
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -0,0 +1,639 @@
+/*
+ * 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.viewmodel
+
+import android.hardware.biometrics.PromptInfo
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.AuthBiometricView
+import com.android.systemui.biometrics.data.repository.FakePromptRepository
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
+import com.android.systemui.biometrics.domain.model.BiometricModalities
+import com.android.systemui.biometrics.domain.model.BiometricModality
+import com.android.systemui.biometrics.extractAuthenticatorTypes
+import com.android.systemui.biometrics.faceSensorPropertiesInternal
+import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+
+private const val USER_ID = 4
+private const val CHALLENGE = 2L
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(Parameterized::class)
+internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCase() {
+
+ @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var lockPatternUtils: LockPatternUtils
+
+ private val testScope = TestScope()
+ private val promptRepository = FakePromptRepository()
+
+ private lateinit var selector: PromptSelectorInteractor
+ private lateinit var viewModel: PromptViewModel
+
+ @Before
+ fun setup() {
+ selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)
+ selector.resetPrompt()
+
+ viewModel = PromptViewModel(selector)
+ }
+
+ @Test
+ fun `start idle and show authenticating`() =
+ runGenericTest(doNotStart = true) {
+ val expectedSize =
+ if (testCase.shouldStartAsImplicitFlow) PromptSize.SMALL else PromptSize.MEDIUM
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val modalities by collectLastValue(viewModel.modalities)
+ val message by collectLastValue(viewModel.message)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isNotAuthenticated).isTrue()
+ with(modalities ?: throw Exception("missing modalities")) {
+ assertThat(hasFace).isEqualTo(testCase.face != null)
+ assertThat(hasFingerprint).isEqualTo(testCase.fingerprint != null)
+ }
+ assertThat(message).isEqualTo(PromptMessage.Empty)
+ assertThat(size).isEqualTo(expectedSize)
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN)
+
+ val startMessage = "here we go"
+ viewModel.showAuthenticating(startMessage, isRetry = false)
+
+ assertThat(message).isEqualTo(PromptMessage.Help(startMessage))
+ assertThat(authenticating).isTrue()
+ assertThat(authenticated?.isNotAuthenticated).isTrue()
+ assertThat(size).isEqualTo(expectedSize)
+ assertButtonsVisible(negative = expectedSize != PromptSize.SMALL)
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING)
+ }
+
+ @Test
+ fun `shows authenticated - no errors`() = runGenericTest {
+ // this case can't happen until fingerprint is started
+ // trigger it now since no error has occurred in this test
+ val forceError = testCase.isCoex && testCase.authenticatedByFingerprint
+
+ if (forceError) {
+ assertThat(viewModel.fingerprintStartMode.first())
+ .isEqualTo(FingerprintStartMode.Pending)
+ viewModel.ensureFingerprintHasStarted(isDelayed = true)
+ }
+
+ showAuthenticated(
+ testCase.authenticatedModality,
+ testCase.expectConfirmation(atLeastOneFailure = forceError),
+ )
+ }
+
+ private suspend fun TestScope.showAuthenticated(
+ authenticatedModality: BiometricModality,
+ expectConfirmation: Boolean,
+ ) {
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val fpStartMode by collectLastValue(viewModel.fingerprintStartMode)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+
+ val authWithSmallPrompt =
+ testCase.shouldStartAsImplicitFlow &&
+ (fpStartMode == FingerprintStartMode.Pending || testCase.isFaceOnly)
+ assertThat(authenticating).isTrue()
+ assertThat(authenticated?.isNotAuthenticated).isTrue()
+ assertThat(size).isEqualTo(if (authWithSmallPrompt) PromptSize.SMALL else PromptSize.MEDIUM)
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATING)
+ assertButtonsVisible(negative = !authWithSmallPrompt)
+
+ val delay = 1000L
+ viewModel.showAuthenticated(authenticatedModality, delay)
+
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(authenticated?.delay).isEqualTo(delay)
+ assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
+ assertThat(size)
+ .isEqualTo(
+ if (authenticatedModality == BiometricModality.Fingerprint || expectConfirmation) {
+ PromptSize.MEDIUM
+ } else {
+ PromptSize.SMALL
+ }
+ )
+ assertThat(legacyState)
+ .isEqualTo(
+ if (expectConfirmation) {
+ AuthBiometricView.STATE_PENDING_CONFIRMATION
+ } else {
+ AuthBiometricView.STATE_AUTHENTICATED
+ }
+ )
+ assertButtonsVisible(
+ cancel = expectConfirmation,
+ confirm = expectConfirmation,
+ )
+ }
+
+ @Test
+ fun `shows temporary errors`() = runGenericTest {
+ val checkAtEnd = suspend { assertButtonsVisible(negative = true) }
+
+ showTemporaryErrors(restart = false) { checkAtEnd() }
+ showTemporaryErrors(restart = false, helpAfterError = "foo") { checkAtEnd() }
+ showTemporaryErrors(restart = true) { checkAtEnd() }
+ }
+
+ private suspend fun TestScope.showTemporaryErrors(
+ restart: Boolean,
+ helpAfterError: String = "",
+ block: suspend TestScope.() -> Unit = {},
+ ) {
+ val errorMessage = "oh no!"
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+ val canTryAgainNow by collectLastValue(viewModel.canTryAgainNow)
+
+ val errorJob = launch {
+ viewModel.showTemporaryError(
+ errorMessage,
+ authenticateAfterError = restart,
+ messageAfterError = helpAfterError,
+ )
+ }
+
+ assertThat(size).isEqualTo(PromptSize.MEDIUM)
+ assertThat(message).isEqualTo(PromptMessage.Error(errorMessage))
+ assertThat(messageVisible).isTrue()
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_ERROR)
+
+ // temporary error should disappear after a delay
+ errorJob.join()
+ if (helpAfterError.isNotBlank()) {
+ assertThat(message).isEqualTo(PromptMessage.Help(helpAfterError))
+ assertThat(messageVisible).isTrue()
+ } else {
+ assertThat(message).isEqualTo(PromptMessage.Empty)
+ assertThat(messageVisible).isFalse()
+ }
+ assertThat(legacyState)
+ .isEqualTo(
+ if (restart) {
+ AuthBiometricView.STATE_AUTHENTICATING
+ } else {
+ AuthBiometricView.STATE_HELP
+ }
+ )
+
+ assertThat(authenticating).isEqualTo(restart)
+ assertThat(authenticated?.isNotAuthenticated).isTrue()
+ assertThat(canTryAgainNow).isFalse()
+
+ block()
+ }
+
+ @Test
+ fun `no errors or temporary help after authenticated`() = runGenericTest {
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val messageIsShowing by collectLastValue(viewModel.isIndicatorMessageVisible)
+ val canTryAgain by collectLastValue(viewModel.canTryAgainNow)
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ val verifyNoError = {
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(message).isEqualTo(PromptMessage.Empty)
+ assertThat(canTryAgain).isFalse()
+ }
+
+ val errorJob = launch { viewModel.showTemporaryError("error") }
+ verifyNoError()
+ errorJob.join()
+ verifyNoError()
+
+ val helpJob = launch { viewModel.showTemporaryHelp("hi") }
+ verifyNoError()
+ helpJob.join()
+ verifyNoError()
+
+ // persistent help is allowed
+ val stickyHelpMessage = "blah"
+ viewModel.showHelp(stickyHelpMessage)
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(message).isEqualTo(PromptMessage.Help(stickyHelpMessage))
+ assertThat(messageIsShowing).isTrue()
+ }
+
+ // @Test
+ fun `suppress errors`() = runGenericTest {
+ val errorMessage = "woot"
+ val message by collectLastValue(viewModel.message)
+
+ val errorJob = launch { viewModel.showTemporaryError(errorMessage) }
+ }
+
+ @Test
+ fun `authenticated at most once`() = runGenericTest {
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ }
+
+ @Test
+ fun `authenticating cannot restart after authenticated`() = runGenericTest {
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+
+ viewModel.showAuthenticating("again!")
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ }
+
+ @Test
+ fun `confirm authentication`() = runGenericTest {
+ val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+ val canTryAgain by collectLastValue(viewModel.canTryAgainNow)
+
+ assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
+ if (expectConfirmation) {
+ assertThat(size).isEqualTo(PromptSize.MEDIUM)
+ assertButtonsVisible(
+ cancel = true,
+ confirm = true,
+ )
+
+ viewModel.confirmAuthenticated()
+ assertThat(message).isEqualTo(PromptMessage.Empty)
+ assertButtonsVisible()
+ }
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_AUTHENTICATED)
+ assertThat(canTryAgain).isFalse()
+ }
+
+ @Test
+ fun `cannot confirm unless authenticated`() = runGenericTest {
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+
+ viewModel.confirmAuthenticated()
+ assertThat(authenticating).isTrue()
+ assertThat(authenticated?.isNotAuthenticated).isTrue()
+
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+
+ // reconfirm should be a no-op
+ viewModel.confirmAuthenticated()
+ viewModel.confirmAuthenticated()
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isNotAuthenticated).isFalse()
+ }
+
+ @Test
+ fun `shows help - before authenticated`() = runGenericTest {
+ val helpMessage = "please help yourself to some cookies"
+ val message by collectLastValue(viewModel.message)
+ val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+
+ viewModel.showHelp(helpMessage)
+
+ assertThat(size).isEqualTo(PromptSize.MEDIUM)
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_HELP)
+ assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
+ assertThat(messageVisible).isTrue()
+
+ assertThat(viewModel.isAuthenticating.first()).isFalse()
+ assertThat(viewModel.isAuthenticated.first().isNotAuthenticated).isTrue()
+ }
+
+ @Test
+ fun `shows help - after authenticated`() = runGenericTest {
+ val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)
+ val helpMessage = "more cookies please"
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+ val size by collectLastValue(viewModel.size)
+ val legacyState by collectLastValue(viewModel.legacyState)
+
+ if (testCase.isCoex && testCase.authenticatedByFingerprint) {
+ viewModel.ensureFingerprintHasStarted(isDelayed = true)
+ }
+ viewModel.showAuthenticated(testCase.authenticatedModality, 0)
+ viewModel.showHelp(helpMessage)
+
+ assertThat(size).isEqualTo(PromptSize.MEDIUM)
+ assertThat(legacyState).isEqualTo(AuthBiometricView.STATE_PENDING_CONFIRMATION)
+ assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
+ assertThat(messageVisible).isTrue()
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isTrue()
+ assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation)
+ assertButtonsVisible(
+ cancel = expectConfirmation,
+ confirm = expectConfirmation,
+ )
+ }
+
+ @Test
+ fun `retries after failure`() = runGenericTest {
+ val errorMessage = "bad"
+ val helpMessage = "again?"
+ val expectTryAgainButton = testCase.isFaceOnly
+ val authenticating by collectLastValue(viewModel.isAuthenticating)
+ val authenticated by collectLastValue(viewModel.isAuthenticated)
+ val message by collectLastValue(viewModel.message)
+ val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible)
+ val canTryAgain by collectLastValue(viewModel.canTryAgainNow)
+
+ viewModel.showAuthenticating("go")
+ val errorJob = launch {
+ viewModel.showTemporaryError(
+ errorMessage,
+ messageAfterError = helpMessage,
+ authenticateAfterError = false,
+ failedModality = testCase.authenticatedModality
+ )
+ }
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isFalse()
+ assertThat(message).isEqualTo(PromptMessage.Error(errorMessage))
+ assertThat(messageVisible).isTrue()
+ assertThat(canTryAgain).isEqualTo(testCase.authenticatedByFace)
+ assertButtonsVisible(negative = true, tryAgain = expectTryAgainButton)
+
+ errorJob.join()
+
+ assertThat(authenticating).isFalse()
+ assertThat(authenticated?.isAuthenticated).isFalse()
+ assertThat(message).isEqualTo(PromptMessage.Help(helpMessage))
+ assertThat(messageVisible).isTrue()
+ assertThat(canTryAgain).isEqualTo(testCase.authenticatedByFace)
+ assertButtonsVisible(negative = true, tryAgain = expectTryAgainButton)
+
+ val helpMessage2 = "foo"
+ viewModel.showAuthenticating(helpMessage2, isRetry = true)
+ assertThat(authenticating).isTrue()
+ assertThat(authenticated?.isAuthenticated).isFalse()
+ assertThat(message).isEqualTo(PromptMessage.Help(helpMessage2))
+ assertThat(messageVisible).isTrue()
+ assertButtonsVisible(negative = true)
+ }
+
+ @Test
+ fun `switch to credential fallback`() = runGenericTest {
+ val size by collectLastValue(viewModel.size)
+
+ // TODO(b/251476085): remove Spaghetti, migrate logic, and update this test
+ viewModel.onSwitchToCredential()
+
+ assertThat(size).isEqualTo(PromptSize.LARGE)
+ }
+
+ /** Asserts that the selected buttons are visible now. */
+ private suspend fun TestScope.assertButtonsVisible(
+ tryAgain: Boolean = false,
+ confirm: Boolean = false,
+ cancel: Boolean = false,
+ negative: Boolean = false,
+ credential: Boolean = false,
+ ) {
+ runCurrent()
+ assertThat(viewModel.isTryAgainButtonVisible.first()).isEqualTo(tryAgain)
+ assertThat(viewModel.isConfirmButtonVisible.first()).isEqualTo(confirm)
+ assertThat(viewModel.isCancelButtonVisible.first()).isEqualTo(cancel)
+ assertThat(viewModel.isNegativeButtonVisible.first()).isEqualTo(negative)
+ assertThat(viewModel.isCredentialButtonVisible.first()).isEqualTo(credential)
+ }
+
+ private fun runGenericTest(
+ doNotStart: Boolean = false,
+ allowCredentialFallback: Boolean = false,
+ block: suspend TestScope.() -> Unit
+ ) {
+ selector.initializePrompt(
+ requireConfirmation = testCase.confirmationRequested,
+ allowCredentialFallback = allowCredentialFallback,
+ fingerprint = testCase.fingerprint,
+ face = testCase.face,
+ )
+
+ // put the view model in the initial authenticating state, unless explicitly skipped
+ val startMode =
+ when {
+ doNotStart -> null
+ testCase.isCoex -> FingerprintStartMode.Delayed
+ else -> FingerprintStartMode.Normal
+ }
+ when (startMode) {
+ FingerprintStartMode.Normal -> {
+ viewModel.ensureFingerprintHasStarted(isDelayed = false)
+ viewModel.showAuthenticating()
+ }
+ FingerprintStartMode.Delayed -> {
+ viewModel.showAuthenticating()
+ }
+ else -> {
+ /* skip */
+ }
+ }
+
+ testScope.runTest { block() }
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data(): Collection<TestCase> = singleModalityTestCases + coexTestCases
+
+ private val singleModalityTestCases =
+ listOf(
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Face,
+ ),
+ TestCase(
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Fingerprint,
+ ),
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Face,
+ confirmationRequested = true,
+ ),
+ TestCase(
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Fingerprint,
+ confirmationRequested = true,
+ ),
+ )
+
+ private val coexTestCases =
+ listOf(
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Face,
+ ),
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Fingerprint,
+ ),
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Face,
+ confirmationRequested = true,
+ ),
+ TestCase(
+ face = faceSensorPropertiesInternal(strong = true).first(),
+ fingerprint = fingerprintSensorPropertiesInternal(strong = true).first(),
+ authenticatedModality = BiometricModality.Fingerprint,
+ confirmationRequested = true,
+ ),
+ )
+ }
+}
+
+internal data class TestCase(
+ val fingerprint: FingerprintSensorPropertiesInternal? = null,
+ val face: FaceSensorPropertiesInternal? = null,
+ val authenticatedModality: BiometricModality,
+ val confirmationRequested: Boolean = false,
+) {
+ override fun toString(): String {
+ val modality =
+ when {
+ fingerprint != null && face != null -> "coex"
+ fingerprint != null -> "fingerprint only"
+ face != null -> "face only"
+ else -> "?"
+ }
+ return "[$modality, by: $authenticatedModality, confirm: $confirmationRequested]"
+ }
+
+ fun expectConfirmation(atLeastOneFailure: Boolean): Boolean =
+ when {
+ isCoex && authenticatedModality == BiometricModality.Face ->
+ atLeastOneFailure || confirmationRequested
+ isFaceOnly -> confirmationRequested
+ else -> false
+ }
+
+ val authenticatedByFingerprint: Boolean
+ get() = authenticatedModality == BiometricModality.Fingerprint
+
+ val authenticatedByFace: Boolean
+ get() = authenticatedModality == BiometricModality.Face
+
+ val isFaceOnly: Boolean
+ get() = face != null && fingerprint == null
+
+ val isFingerprintOnly: Boolean
+ get() = face == null && fingerprint != null
+
+ val isCoex: Boolean
+ get() = face != null && fingerprint != null
+
+ val shouldStartAsImplicitFlow: Boolean
+ get() = (isFaceOnly || isCoex) && !confirmationRequested
+}
+
+/** Initialize the test by selecting the give [fingerprint] or [face] configuration(s). */
+private fun PromptSelectorInteractor.initializePrompt(
+ fingerprint: FingerprintSensorPropertiesInternal? = null,
+ face: FaceSensorPropertiesInternal? = null,
+ requireConfirmation: Boolean = false,
+ allowCredentialFallback: Boolean = false,
+) {
+ val info =
+ PromptInfo().apply {
+ title = "t"
+ subtitle = "s"
+ authenticators = listOf(face, fingerprint).extractAuthenticatorTypes()
+ isDeviceCredentialAllowed = allowCredentialFallback
+ isConfirmationRequested = requireConfirmation
+ }
+ useBiometricsForAuthentication(
+ info,
+ requireConfirmation,
+ USER_ID,
+ CHALLENGE,
+ BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face),
+ )
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 730f89d..9f5c181 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -27,6 +27,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -75,7 +76,7 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
underTest.clearMessage()
- assertThat(message).isNull()
+ assertThat(message).isEmpty()
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
@@ -107,7 +108,7 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
underTest.clearMessage()
- assertThat(message).isNull()
+ assertThat(message).isEmpty()
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
@@ -139,7 +140,7 @@
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
underTest.clearMessage()
- assertThat(message).isNull()
+ assertThat(message).isEmpty()
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
@@ -201,6 +202,56 @@
assertThat(message).isEqualTo(customMessage)
}
+ @Test
+ fun throttling() =
+ testScope.runTest {
+ val throttling by collectLastValue(underTest.throttling)
+ val message by collectLastValue(underTest.message)
+ val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
+ authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+ assertThat(throttling).isNull()
+ assertThat(message).isEqualTo("")
+ assertThat(isUnlocked).isFalse()
+ repeat(BouncerInteractor.THROTTLE_EVERY) { times ->
+ // Wrong PIN.
+ underTest.authenticate(listOf(6, 7, 8, 9))
+ if (times < BouncerInteractor.THROTTLE_EVERY - 1) {
+ assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
+ }
+ }
+ assertThat(throttling).isNotNull()
+ assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
+
+ // Correct PIN, but throttled, so doesn't unlock:
+ underTest.authenticate(listOf(1, 2, 3, 4))
+ assertThat(isUnlocked).isFalse()
+ assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC)
+
+ throttling?.totalDurationSec?.let { seconds ->
+ repeat(seconds) { time ->
+ advanceTimeBy(1000)
+ val remainingTime = seconds - time - 1
+ if (remainingTime > 0) {
+ assertTryAgainMessage(message, remainingTime)
+ }
+ }
+ }
+ assertThat(message).isEqualTo("")
+ assertThat(throttling).isNull()
+ assertThat(isUnlocked).isFalse()
+
+ // Correct PIN and no longer throttled so unlocks:
+ underTest.authenticate(listOf(1, 2, 3, 4))
+ assertThat(isUnlocked).isTrue()
+ }
+
+ private fun assertTryAgainMessage(
+ message: String?,
+ time: Int,
+ ) {
+ assertThat(message).isEqualTo("Try again in $time seconds.")
+ }
+
companion object {
private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 954e67d..b942ccb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -19,11 +19,15 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -40,13 +44,12 @@
utils.authenticationInteractor(
repository = utils.authenticationRepository(),
)
- private val underTest =
- utils.bouncerViewModel(
- utils.bouncerInteractor(
- authenticationInteractor = authenticationInteractor,
- sceneInteractor = utils.sceneInteractor(),
- )
+ private val bouncerInteractor =
+ utils.bouncerInteractor(
+ authenticationInteractor = authenticationInteractor,
+ sceneInteractor = utils.sceneInteractor(),
)
+ private val underTest = utils.bouncerViewModel(bouncerInteractor)
@Test
fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() =
@@ -89,6 +92,65 @@
.isEqualTo(AuthenticationMethodModel::class.sealedSubclasses.toSet())
}
+ @Test
+ fun isMessageUpdateAnimationsEnabled() =
+ testScope.runTest {
+ val isMessageUpdateAnimationsEnabled by
+ collectLastValue(underTest.isMessageUpdateAnimationsEnabled)
+ val throttling by collectLastValue(bouncerInteractor.throttling)
+ authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+ assertThat(isMessageUpdateAnimationsEnabled).isTrue()
+
+ repeat(BouncerInteractor.THROTTLE_EVERY) {
+ // Wrong PIN.
+ bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+ }
+ assertThat(isMessageUpdateAnimationsEnabled).isFalse()
+
+ throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
+ assertThat(isMessageUpdateAnimationsEnabled).isTrue()
+ }
+
+ @Test
+ fun isInputEnabled() =
+ testScope.runTest {
+ val isInputEnabled by
+ collectLastValue(
+ underTest.authMethod.flatMapLatest { authViewModel ->
+ authViewModel?.isInputEnabled ?: emptyFlow()
+ }
+ )
+ val throttling by collectLastValue(bouncerInteractor.throttling)
+ authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+ assertThat(isInputEnabled).isTrue()
+
+ repeat(BouncerInteractor.THROTTLE_EVERY) {
+ // Wrong PIN.
+ bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+ }
+ assertThat(isInputEnabled).isFalse()
+
+ throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) }
+ assertThat(isInputEnabled).isTrue()
+ }
+
+ @Test
+ fun throttlingDialogMessage() =
+ testScope.runTest {
+ val throttlingDialogMessage by collectLastValue(underTest.throttlingDialogMessage)
+ authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
+
+ repeat(BouncerInteractor.THROTTLE_EVERY) {
+ // Wrong PIN.
+ assertThat(throttlingDialogMessage).isNull()
+ bouncerInteractor.authenticate(listOf(3, 4, 5, 6))
+ }
+ assertThat(throttlingDialogMessage).isNotEmpty()
+
+ underTest.onThrottlingDialogDismissed()
+ assertThat(throttlingDialogMessage).isNull()
+ }
+
private fun authMethodsToTest(): List<AuthenticationMethodModel> {
return listOf(
AuthenticationMethodModel.None,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index e48b638..b7b90de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -26,6 +26,8 @@
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -57,6 +59,7 @@
private val underTest =
PasswordBouncerViewModel(
interactor = bouncerInteractor,
+ isInputEnabled = MutableStateFlow(true).asStateFlow(),
)
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 6ce29e6..b588ba2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -27,6 +27,8 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -60,6 +62,7 @@
applicationContext = context,
applicationScope = testScope.backgroundScope,
interactor = bouncerInteractor,
+ isInputEnabled = MutableStateFlow(true).asStateFlow(),
)
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index bb28520..83f9687 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -27,6 +27,8 @@
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@@ -68,6 +70,7 @@
PinBouncerViewModel(
applicationScope = testScope.backgroundScope,
interactor = bouncerInteractor,
+ isInputEnabled = MutableStateFlow(true).asStateFlow(),
)
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index f4cd383..1643e17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -28,7 +28,6 @@
import android.content.ComponentName;
import android.graphics.Rect;
-import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.hardware.biometrics.PromptInfo;
import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
@@ -443,15 +442,13 @@
final long operationId = 1;
final String packageName = "test";
final long requestId = 10;
- final int multiSensorConfig = BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
mCommandQueue.showAuthenticationDialog(promptInfo, receiver, sensorIds,
- credentialAllowed, requireConfirmation, userId, operationId, packageName, requestId,
- multiSensorConfig);
+ credentialAllowed, requireConfirmation, userId, operationId, packageName, requestId);
waitForIdleSync();
verify(mCallbacks).showAuthenticationDialog(eq(promptInfo), eq(receiver), eq(sensorIds),
eq(credentialAllowed), eq(requireConfirmation), eq(userId), eq(operationId),
- eq(packageName), eq(requestId), eq(multiSensorConfig));
+ eq(packageName), eq(requestId));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 45a37cf..8f725be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -35,7 +35,6 @@
import android.content.res.Configuration;
import android.media.AudioManager;
import android.os.SystemClock;
-import android.provider.DeviceConfig;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.Gravity;
@@ -47,7 +46,6 @@
import androidx.test.filters.SmallTest;
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.systemui.Prefs;
import com.android.systemui.R;
@@ -62,9 +60,6 @@
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.FakeConfigurationController;
-import com.android.systemui.util.DeviceConfigProxyFake;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
import org.junit.After;
import org.junit.Before;
@@ -88,8 +83,6 @@
View mDrawerVibrate;
View mDrawerMute;
View mDrawerNormal;
- private DeviceConfigProxyFake mDeviceConfigProxy;
- private FakeExecutor mExecutor;
private TestableLooper mTestableLooper;
private ConfigurationController mConfigurationController;
private int mOriginalOrientation;
@@ -131,8 +124,6 @@
getContext().addMockSystemService(KeyguardManager.class, mKeyguard);
mTestableLooper = TestableLooper.get(this);
- mDeviceConfigProxy = new DeviceConfigProxyFake();
- mExecutor = new FakeExecutor(new FakeSystemClock());
when(mPostureController.getDevicePosture())
.thenReturn(DevicePostureController.DEVICE_POSTURE_CLOSED);
@@ -151,8 +142,6 @@
mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor,
- mDeviceConfigProxy,
- mExecutor,
mCsdWarningDialogFactory,
mPostureController,
mTestableLooper.getLooper(),
@@ -173,9 +162,6 @@
VolumePrefs.SHOW_RINGER_TOAST_COUNT + 1);
Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false);
-
- mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false);
}
private State createShellState() {
@@ -351,13 +337,8 @@
* API does not exist. So we do the next best thing; we check the cached icon id.
*/
@Test
- public void notificationVolumeSeparated_theRingerIconChanges() {
- mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "true", false);
-
- mExecutor.runAllReady(); // for the config change to take effect
-
- // assert icon is new based on res id
+ public void notificationVolumeSeparated_theRingerIconChangesToSpeakerIcon() {
+ // already separated. assert icon is new based on res id
assertEquals(mDialog.mVolumeRingerIconDrawableId,
R.drawable.ic_speaker_on);
assertEquals(mDialog.mVolumeRingerMuteIconDrawableId,
@@ -365,17 +346,6 @@
}
@Test
- public void notificationVolumeNotSeparated_theRingerIconRemainsTheSame() {
- mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false);
-
- mExecutor.runAllReady();
-
- assertEquals(mDialog.mVolumeRingerIconDrawableId, R.drawable.ic_volume_ringer);
- assertEquals(mDialog.mVolumeRingerMuteIconDrawableId, R.drawable.ic_volume_ringer_mute);
- }
-
- @Test
public void testDialogDismissAnimation_notifyVisibleIsNotCalledBeforeAnimation() {
mDialog.dismissH(DISMISS_REASON_UNKNOWN);
// notifyVisible(false) should not be called immediately but only after the dismiss
@@ -408,8 +378,6 @@
mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor,
- mDeviceConfigProxy,
- mExecutor,
mCsdWarningDialogFactory,
devicePostureController,
mTestableLooper.getLooper(),
@@ -447,8 +415,6 @@
mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor,
- mDeviceConfigProxy,
- mExecutor,
mCsdWarningDialogFactory,
devicePostureController,
mTestableLooper.getLooper(),
@@ -485,8 +451,6 @@
mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor,
- mDeviceConfigProxy,
- mExecutor,
mCsdWarningDialogFactory,
devicePostureController,
mTestableLooper.getLooper(),
@@ -525,8 +489,6 @@
mVolumePanelFactory,
mActivityStarter,
mInteractionJankMonitor,
- mDeviceConfigProxy,
- mExecutor,
mCsdWarningDialogFactory,
mPostureController,
mTestableLooper.getLooper(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
index 96658c6..d270700 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt
@@ -1,7 +1,7 @@
package com.android.systemui.biometrics.data.repository
import android.hardware.biometrics.PromptInfo
-import com.android.systemui.biometrics.data.model.PromptKind
+import com.android.systemui.biometrics.shared.model.PromptKind
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -20,26 +20,32 @@
private var _challenge = MutableStateFlow<Long?>(null)
override val challenge = _challenge.asStateFlow()
- private val _kind = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
+ private val _kind = MutableStateFlow<PromptKind>(PromptKind.Biometric())
override val kind = _kind.asStateFlow()
+ private val _isConfirmationRequired = MutableStateFlow(false)
+ override val isConfirmationRequired = _isConfirmationRequired.asStateFlow()
+
override fun setPrompt(
promptInfo: PromptInfo,
userId: Int,
gatekeeperChallenge: Long?,
- kind: PromptKind
+ kind: PromptKind,
+ requireConfirmation: Boolean,
) {
_promptInfo.value = promptInfo
_userId.value = userId
_challenge.value = gatekeeperChallenge
_kind.value = kind
+ _isConfirmationRequired.value = requireConfirmation
}
override fun unsetPrompt() {
_promptInfo.value = null
_userId.value = null
_challenge.value = null
- _kind.value = PromptKind.ANY_BIOMETRIC
+ _kind.value = PromptKind.Biometric()
+ _isConfirmationRequired.value = false
}
fun setIsShowing(showing: Boolean) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 0da25be..31d60f2 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -8297,8 +8297,8 @@
r.mFgsDelegation != null ? r.mFgsDelegation.mOptions.mDelegationService
: ForegroundServiceDelegationOptions.DELEGATION_SERVICE_DEFAULT,
0 /* api_sate */,
- 0 /* api_type */,
- 0 /* api_timestamp */,
+ null /* api_type */,
+ null /* api_timestamp */,
mAm.getUidStateLocked(r.appInfo.uid),
mAm.getUidProcessCapabilityLocked(r.appInfo.uid),
mAm.getUidStateLocked(r.mRecentCallingUid),
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 030d596..8c1fd51 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -292,6 +292,15 @@
private static final String KEY_CORE_DEFER_UNTIL_ACTIVE = "bcast_core_defer_until_active";
private static final boolean DEFAULT_CORE_DEFER_UNTIL_ACTIVE = true;
+ /**
+ * For {@link BroadcastQueueModernImpl}: How frequently we should check for the pending
+ * cold start validity.
+ */
+ public long PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 30 * 1000;
+ private static final String KEY_PENDING_COLD_START_CHECK_INTERVAL_MILLIS =
+ "pending_cold_start_check_interval_millis";
+ private static final long DEFAULT_PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 30_000;
+
// Settings override tracking for this instance
private String mSettingsKey;
private SettingsObserver mSettingsObserver;
@@ -441,6 +450,9 @@
DEFAULT_MAX_HISTORY_SUMMARY_SIZE);
CORE_DEFER_UNTIL_ACTIVE = getDeviceConfigBoolean(KEY_CORE_DEFER_UNTIL_ACTIVE,
DEFAULT_CORE_DEFER_UNTIL_ACTIVE);
+ PENDING_COLD_START_CHECK_INTERVAL_MILLIS = getDeviceConfigLong(
+ KEY_PENDING_COLD_START_CHECK_INTERVAL_MILLIS,
+ DEFAULT_PENDING_COLD_START_CHECK_INTERVAL_MILLIS);
}
// TODO: migrate BroadcastRecord to accept a BroadcastConstants
@@ -499,6 +511,8 @@
MAX_CONSECUTIVE_NORMAL_DISPATCHES).println();
pw.print(KEY_CORE_DEFER_UNTIL_ACTIVE,
CORE_DEFER_UNTIL_ACTIVE).println();
+ pw.print(KEY_PENDING_COLD_START_CHECK_INTERVAL_MILLIS,
+ PENDING_COLD_START_CHECK_INTERVAL_MILLIS).println();
pw.decreaseIndent();
pw.println();
}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index d6e692c..f180f02 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -248,6 +248,7 @@
private static final int MSG_DELIVERY_TIMEOUT_HARD = 3;
private static final int MSG_BG_ACTIVITY_START_TIMEOUT = 4;
private static final int MSG_CHECK_HEALTH = 5;
+ private static final int MSG_CHECK_PENDING_COLD_START_VALIDITY = 6;
private void enqueueUpdateRunningList() {
mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST);
@@ -284,6 +285,10 @@
checkHealth();
return true;
}
+ case MSG_CHECK_PENDING_COLD_START_VALIDITY: {
+ checkPendingColdStartValidity();
+ return true;
+ }
}
return false;
};
@@ -450,10 +455,14 @@
// skip to look for another warm process
if (mRunningColdStart == null) {
mRunningColdStart = queue;
- } else {
+ } else if (isPendingColdStartValid()) {
// Move to considering next runnable queue
queue = nextQueue;
continue;
+ } else {
+ // Pending cold start is not valid, so clear it and move on.
+ clearInvalidPendingColdStart();
+ mRunningColdStart = queue;
}
}
@@ -486,11 +495,46 @@
mService.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_RECEIVER);
}
+ checkPendingColdStartValidity();
checkAndRemoveWaitingFor();
traceEnd(cookie);
}
+ private boolean isPendingColdStartValid() {
+ if (mRunningColdStart.app.getPid() > 0) {
+ // If the process has already started, check if it wasn't killed.
+ return !mRunningColdStart.app.isKilled();
+ } else {
+ // Otherwise, check if the process start is still pending.
+ return mRunningColdStart.app.isPendingStart();
+ }
+ }
+
+ private void clearInvalidPendingColdStart() {
+ logw("Clearing invalid pending cold start: " + mRunningColdStart);
+ onApplicationCleanupLocked(mRunningColdStart.app);
+ }
+
+ private void checkPendingColdStartValidity() {
+ // There are a few cases where a starting process gets killed but AMS doesn't report
+ // this event. So, once we start waiting for a pending cold start, periodically check
+ // if the pending start is still valid and if not, clear it so that the queue doesn't
+ // keep waiting for the process start forever.
+ synchronized (mService) {
+ // If there is no pending cold start, then nothing to do.
+ if (mRunningColdStart == null) {
+ return;
+ }
+ if (isPendingColdStartValid()) {
+ mLocalHandler.sendEmptyMessageDelayed(MSG_CHECK_PENDING_COLD_START_VALIDITY,
+ mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS);
+ } else {
+ clearInvalidPendingColdStart();
+ }
+ }
+ }
+
@Override
public boolean onApplicationAttachedLocked(@NonNull ProcessRecord app) {
// Process records can be recycled, so always start by looking up the
diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
index daa4ba4..9b3f249 100644
--- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
+++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
@@ -451,6 +451,10 @@
@ForegroundServiceApiType int apiType, long timestamp) {
final long apiDurationBeforeFgsStart = r.mFgsEnterTime - timestamp;
final long apiDurationAfterFgsEnd = timestamp - r.mFgsExitTime;
+ final int[] apiTypes = new int[1];
+ apiTypes[0] = apiType;
+ final long[] timeStamps = new long[1];
+ timeStamps[0] = timestamp;
FrameworkStatsLog.write(FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED,
r.appInfo.uid,
r.shortInstanceName,
@@ -475,8 +479,8 @@
r.mFgsDelegation != null ? r.mFgsDelegation.mOptions.mDelegationService
: ForegroundServiceDelegationOptions.DELEGATION_SERVICE_DEFAULT,
apiState,
- apiType,
- timestamp,
+ apiTypes,
+ timeStamps,
ActivityManager.PROCESS_STATE_UNKNOWN,
ActivityManager.PROCESS_CAPABILITY_NONE,
ActivityManager.PROCESS_STATE_UNKNOWN,
@@ -500,6 +504,10 @@
apiDurationAfterFgsEnd = timestamp - uidState.mLastFgsTimeStamp.get(apiType);
}
}
+ final int[] apiTypes = new int[1];
+ apiTypes[0] = apiType;
+ final long[] timeStamps = new long[1];
+ timeStamps[0] = timestamp;
FrameworkStatsLog.write(FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED,
uid,
null,
@@ -522,8 +530,8 @@
0,
0,
apiState,
- apiType,
- timestamp,
+ apiTypes,
+ timeStamps,
ActivityManager.PROCESS_STATE_UNKNOWN,
ActivityManager.PROCESS_CAPABILITY_NONE,
ActivityManager.PROCESS_STATE_UNKNOWN,
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index ab4fb46..202d407 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -349,21 +349,22 @@
* use caller's BAL permission.
*/
public static BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCaller(
- @Nullable ActivityOptions activityOptions, int callingUid) {
+ @Nullable ActivityOptions activityOptions, int callingUid,
+ @Nullable String callingPackage) {
if (activityOptions == null) {
// since the ActivityOptions were not created by the app itself, determine the default
// for the app
- return getDefaultBackgroundStartPrivileges(callingUid);
+ return getDefaultBackgroundStartPrivileges(callingUid, callingPackage);
}
return getBackgroundStartPrivilegesAllowedByCaller(activityOptions.toBundle(),
- callingUid);
+ callingUid, callingPackage);
}
private static BackgroundStartPrivileges getBackgroundStartPrivilegesAllowedByCaller(
- @Nullable Bundle options, int callingUid) {
+ @Nullable Bundle options, int callingUid, @Nullable String callingPackage) {
if (options == null || !options.containsKey(
ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)) {
- return getDefaultBackgroundStartPrivileges(callingUid);
+ return getDefaultBackgroundStartPrivileges(callingUid, callingPackage);
}
return options.getBoolean(ActivityOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED)
? BackgroundStartPrivileges.ALLOW_BAL
@@ -382,7 +383,7 @@
android.Manifest.permission.LOG_COMPAT_CHANGE
})
public static BackgroundStartPrivileges getDefaultBackgroundStartPrivileges(
- int callingUid) {
+ int callingUid, @Nullable String callingPackage) {
if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {
// We temporarily allow BAL for system processes, while we verify that all valid use
// cases are opted in explicitly to grant their BAL permission.
@@ -391,7 +392,9 @@
// as soon as that app is upgraded (or removed) BAL would be blocked. (b/283138430)
return BackgroundStartPrivileges.ALLOW_BAL;
}
- boolean isChangeEnabledForApp = CompatChanges.isChangeEnabled(
+ boolean isChangeEnabledForApp = callingPackage != null ? CompatChanges.isChangeEnabled(
+ DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage,
+ UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled(
DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingUid);
if (isChangeEnabledForApp) {
return BackgroundStartPrivileges.ALLOW_FGS;
@@ -647,7 +650,7 @@
// temporarily allow receivers and services to open activities from background if the
// PendingIntent.send() caller was foreground at the time of sendInner() call
if (uid != callingUid && controller.mAtmInternal.isUidForeground(callingUid)) {
- return getBackgroundStartPrivilegesAllowedByCaller(options, callingUid);
+ return getBackgroundStartPrivilegesAllowedByCaller(options, callingUid, null);
}
return BackgroundStartPrivileges.NONE;
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 355981a..bb04c35 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -45,7 +45,6 @@
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
-import android.app.ActivityThread;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
@@ -167,7 +166,6 @@
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorManager;
-import android.provider.DeviceConfig;
import android.provider.Settings;
import android.provider.Settings.System;
import android.service.notification.ZenModeConfig;
@@ -187,10 +185,8 @@
import android.view.accessibility.AccessibilityManager;
import android.widget.Toast;
-
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.Preconditions;
@@ -252,7 +248,6 @@
AudioSystemAdapter.OnVolRangeInitRequestListener {
private static final String TAG = "AS.AudioService";
- private static final boolean CONFIG_DEFAULT_VAL = false;
private final AudioSystemAdapter mAudioSystem;
private final SystemServerAdapter mSystemServer;
@@ -309,7 +304,7 @@
* indicates whether STREAM_NOTIFICATION is aliased to STREAM_RING
* not final due to test method, see {@link #setNotifAliasRingForTest(boolean)}.
*/
- private boolean mNotifAliasRing;
+ private boolean mNotifAliasRing = false;
/**
* Test method to temporarily override whether STREAM_NOTIFICATION is aliased to STREAM_RING,
@@ -1057,13 +1052,6 @@
mUseVolumeGroupAliases = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_handleVolumeAliasesUsingVolumeGroups);
- mNotifAliasRing = !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
- SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false);
-
- DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
- ActivityThread.currentApplication().getMainExecutor(),
- this::onDeviceConfigChange);
-
// Initialize volume
// Priority 1 - Android Property
// Priority 2 - Audio Policy Service
@@ -1277,22 +1265,6 @@
}
/**
- * Separating notification volume from ring is NOT of aliasing the corresponding streams
- * @param properties
- */
- private void onDeviceConfigChange(DeviceConfig.Properties properties) {
- Set<String> changeSet = properties.getKeyset();
- if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) {
- boolean newNotifAliasRing = !properties.getBoolean(
- SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL);
- if (mNotifAliasRing != newNotifAliasRing) {
- mNotifAliasRing = newNotifAliasRing;
- updateStreamVolumeAlias(true, TAG);
- }
- }
- }
-
- /**
* Called by handling of MSG_INIT_STREAMS_VOLUMES
*/
private void onInitStreamsAndVolumes() {
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index bf5e8ee..1989bc7 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -21,8 +21,6 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR_BASE;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;
@@ -44,7 +42,6 @@
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricManager;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.IBiometricSensorReceiver;
@@ -68,7 +65,6 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
@@ -134,7 +130,6 @@
// The current state, which can be either idle, called, or started
private @SessionState int mState = STATE_AUTH_IDLE;
- private @BiometricMultiSensorMode int mMultiSensorMode;
private int[] mSensors;
// TODO(b/197265902): merge into state
private boolean mCancelled;
@@ -255,7 +250,6 @@
// SystemUI invokes that path.
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
mSensors = new int[0];
- mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
mStatusBarService.showAuthenticationDialog(
mPromptInfo,
@@ -266,8 +260,7 @@
mUserId,
mOperationId,
mOpPackageName,
- mRequestId,
- mMultiSensorMode);
+ mRequestId);
} else if (!mPreAuthInfo.eligibleSensors.isEmpty()) {
// Some combination of biometric or biometric|credential is requested
setSensorsToStateWaitingForCookie(false /* isTryAgain */);
@@ -310,8 +303,6 @@
for (int i = 0; i < mPreAuthInfo.eligibleSensors.size(); i++) {
mSensors[i] = mPreAuthInfo.eligibleSensors.get(i).id;
}
- mMultiSensorMode = getMultiSensorModeForNewSession(
- mPreAuthInfo.eligibleSensors);
mStatusBarService.showAuthenticationDialog(mPromptInfo,
mSysuiReceiver,
@@ -321,8 +312,7 @@
mUserId,
mOperationId,
mOpPackageName,
- mRequestId,
- mMultiSensorMode);
+ mRequestId);
mState = STATE_AUTH_STARTED;
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
@@ -438,7 +428,6 @@
mPromptInfo.setAuthenticators(authenticators);
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
- mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT;
mSensors = new int[0];
mStatusBarService.showAuthenticationDialog(
@@ -450,8 +439,7 @@
mUserId,
mOperationId,
mOpPackageName,
- mRequestId,
- mMultiSensorMode);
+ mRequestId);
} else {
mClientReceiver.onError(modality, error, vendorCode);
return true;
@@ -545,13 +533,30 @@
}
}
- void onDialogAnimatedIn() {
+ void onDialogAnimatedIn(boolean startFingerprintNow) {
if (mState != STATE_AUTH_STARTED) {
Slog.e(TAG, "onDialogAnimatedIn, unexpected state: " + mState);
return;
}
mState = STATE_AUTH_STARTED_UI_SHOWING;
+ if (startFingerprintNow) {
+ startAllPreparedFingerprintSensors();
+ } else {
+ Slog.d(TAG, "delaying fingerprint sensor start");
+ }
+ }
+
+ // call once anytime after onDialogAnimatedIn() to indicate it's appropriate to start the
+ // fingerprint sensor (i.e. face auth has failed or is not available)
+ void onStartFingerprint() {
+ if (mState != STATE_AUTH_STARTED
+ && mState != STATE_AUTH_STARTED_UI_SHOWING
+ && mState != STATE_AUTH_PAUSED
+ && mState != STATE_ERROR_PENDING_SYSUI) {
+ Slog.w(TAG, "onStartFingerprint, started from unexpected state: " + mState);
+ }
+
startAllPreparedFingerprintSensors();
}
@@ -919,25 +924,6 @@
}
}
- @BiometricMultiSensorMode
- private static int getMultiSensorModeForNewSession(Collection<BiometricSensor> sensors) {
- boolean hasFace = false;
- boolean hasFingerprint = false;
-
- for (BiometricSensor sensor: sensors) {
- if (sensor.modality == TYPE_FACE) {
- hasFace = true;
- } else if (sensor.modality == TYPE_FINGERPRINT) {
- hasFingerprint = true;
- }
- }
-
- if (hasFace && hasFingerprint) {
- return BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE;
- }
- return BIOMETRIC_MULTI_SENSOR_DEFAULT;
- }
-
@Override
public String toString() {
return "State: " + mState
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 4488434..0942d85 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -480,8 +480,13 @@
}
@Override
- public void onDialogAnimatedIn() {
- mHandler.post(() -> handleOnDialogAnimatedIn(requestId));
+ public void onDialogAnimatedIn(boolean startFingerprintNow) {
+ mHandler.post(() -> handleOnDialogAnimatedIn(requestId, startFingerprintNow));
+ }
+
+ @Override
+ public void onStartFingerprintNow() {
+ mHandler.post(() -> handleOnStartFingerprintNow(requestId));
}
};
}
@@ -1237,7 +1242,7 @@
}
}
- private void handleOnDialogAnimatedIn(long requestId) {
+ private void handleOnDialogAnimatedIn(long requestId, boolean startFingerprintNow) {
Slog.d(TAG, "handleOnDialogAnimatedIn");
final AuthSession session = getAuthSessionIfCurrent(requestId);
@@ -1246,7 +1251,19 @@
return;
}
- session.onDialogAnimatedIn();
+ session.onDialogAnimatedIn(startFingerprintNow);
+ }
+
+ private void handleOnStartFingerprintNow(long requestId) {
+ Slog.d(TAG, "handleOnStartFingerprintNow");
+
+ final AuthSession session = getAuthSessionIfCurrent(requestId);
+ if (session == null) {
+ Slog.w(TAG, "handleOnStartFingerprintNow: AuthSession is not current");
+ return;
+ }
+
+ session.onStartFingerprint();
}
/**
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
index 6a01042..42b2682 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -16,6 +16,8 @@
package com.android.server.broadcastradio;
+import android.Manifest;
+import android.content.pm.PackageManager;
import android.hardware.broadcastradio.IBroadcastRadio;
import android.hardware.radio.IAnnouncementListener;
import android.hardware.radio.ICloseHandle;
@@ -23,6 +25,7 @@
import android.hardware.radio.ITuner;
import android.hardware.radio.ITunerCallback;
import android.hardware.radio.RadioManager;
+import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -112,6 +115,13 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
+ if (mService.getContext().checkCallingOrSelfPermission(Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+ printWriter.println("Permission Denial: can't dump AIDL BroadcastRadioService from "
+ + "from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+ + " without permission " + Manifest.permission.DUMP);
+ return;
+ }
IndentingPrintWriter radioPrintWriter = new IndentingPrintWriter(printWriter);
radioPrintWriter.printf("BroadcastRadioService\n");
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index 408fba1..bc72a4b 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -16,12 +16,15 @@
package com.android.server.broadcastradio;
+import android.Manifest;
+import android.content.pm.PackageManager;
import android.hardware.radio.IAnnouncementListener;
import android.hardware.radio.ICloseHandle;
import android.hardware.radio.IRadioService;
import android.hardware.radio.ITuner;
import android.hardware.radio.ITunerCallback;
import android.hardware.radio.RadioManager;
+import android.os.Binder;
import android.os.RemoteException;
import android.util.IndentingPrintWriter;
import android.util.Log;
@@ -129,6 +132,13 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mService.getContext().checkCallingOrSelfPermission(Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+ pw.println("Permission Denial: can't dump HIDL BroadcastRadioService from "
+ + "from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
+ + " without permission " + Manifest.permission.DUMP);
+ return;
+ }
IndentingPrintWriter radioPw = new IndentingPrintWriter(pw);
radioPw.printf("BroadcastRadioService\n");
diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java
index 7a5664f..5288e85 100644
--- a/services/core/java/com/android/server/policy/AppOpsPolicy.java
+++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java
@@ -37,6 +37,7 @@
import android.os.IBinder;
import android.os.PackageTagsList;
import android.os.Process;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.service.voice.VoiceInteractionManagerInternal;
import android.service.voice.VoiceInteractionManagerInternal.HotwordDetectionServiceIdentity;
@@ -68,6 +69,8 @@
private static final String ACTIVITY_RECOGNITION_TAGS =
"android:activity_recognition_allow_listed_tags";
private static final String ACTIVITY_RECOGNITION_TAGS_SEPARATOR = ";";
+ private static final boolean SYSPROP_HOTWORD_DETECTION_SERVICE_REQUIRED =
+ SystemProperties.getBoolean("ro.hotword.detection_service_required", false);
@NonNull
private final Object mLock = new Object();
@@ -199,10 +202,16 @@
}
}
- private static boolean isHotwordDetectionServiceRequired(PackageManager pm) {
+ /**
+ * @hide
+ */
+ public static boolean isHotwordDetectionServiceRequired(PackageManager pm) {
// The HotwordDetectionService APIs aren't ready yet for Auto or TV.
- return !(pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
- || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+ if (pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+ || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+ return false;
+ }
+ return SYSPROP_HOTWORD_DETECTION_SERVICE_REQUIRED;
}
@Override
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 363d2fd..044d30b 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -53,7 +53,6 @@
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Icon;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
-import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.hardware.biometrics.IBiometricContextListener;
import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.hardware.biometrics.PromptInfo;
@@ -949,14 +948,12 @@
@Override
public void showAuthenticationDialog(PromptInfo promptInfo, IBiometricSysuiReceiver receiver,
int[] sensorIds, boolean credentialAllowed, boolean requireConfirmation,
- int userId, long operationId, String opPackageName, long requestId,
- @BiometricMultiSensorMode int multiSensorConfig) {
+ int userId, long operationId, String opPackageName, long requestId) {
enforceBiometricDialog();
if (mBar != null) {
try {
mBar.showAuthenticationDialog(promptInfo, receiver, sensorIds, credentialAllowed,
- requireConfirmation, userId, operationId, opPackageName, requestId,
- multiSensorConfig);
+ requireConfirmation, userId, operationId, opPackageName, requestId);
} catch (RemoteException ex) {
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d84c013..3db0315 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3570,6 +3570,14 @@
// Tell window manager to prepare for this one to be removed.
setVisibility(false);
+ // Propagate the last IME visibility in the same task, so the IME can show
+ // automatically if the next activity has a focused editable view.
+ if (mLastImeShown && mTransitionController.isShellTransitionsEnabled()) {
+ final ActivityRecord nextRunning = task.topRunningActivity();
+ if (nextRunning != null) {
+ nextRunning.mLastImeShown = true;
+ }
+ }
if (getTaskFragment().getPausingActivity() == null) {
ProtoLog.v(WM_DEBUG_STATES, "Finish needs to pause: %s", this);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 1360a95..750ed98 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5342,6 +5342,12 @@
return null;
}
+ /**
+ * Returns the {@link WindowProcessController} for the app process for the given uid and pid.
+ *
+ * If no such {@link WindowProcessController} is found, it does not belong to an app, or the
+ * pid does not match the uid {@code null} is returned.
+ */
WindowProcessController getProcessController(int pid, int uid) {
final WindowProcessController proc = mProcessMap.getProcess(pid);
if (proc == null) return null;
@@ -5351,6 +5357,27 @@
return null;
}
+ /**
+ * Returns the package name if (and only if) the package name can be uniquely determined.
+ * Otherwise returns {@code null}.
+ *
+ * The provided pid must match the provided uid, otherwise this also returns null.
+ */
+ @Nullable String getPackageNameIfUnique(int uid, int pid) {
+ final WindowProcessController proc = mProcessMap.getProcess(pid);
+ if (proc == null || proc.mUid != uid) {
+ Slog.w(TAG, "callingPackage for (uid=" + uid + ", pid=" + pid + ") has no WPC");
+ return null;
+ }
+ List<String> realCallingPackages = proc.getPackageList();
+ if (realCallingPackages.size() != 1) {
+ Slog.w(TAG, "callingPackage for (uid=" + uid + ", pid=" + pid + ") is ambiguous: "
+ + realCallingPackages);
+ return null;
+ }
+ return realCallingPackages.get(0);
+ }
+
/** A uid is considered to be foreground if it has a visible non-toast window. */
@HotPath(caller = HotPath.START_SERVICE)
boolean hasActiveVisibleWindow(int uid) {
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index dc49e8c..b216578 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -180,7 +180,8 @@
Intent intent,
ActivityOptions checkedOptions) {
return checkBackgroundActivityStart(callingUid, callingPid, callingPackage,
- realCallingUid, realCallingPid, callerApp, originatingPendingIntent,
+ realCallingUid, realCallingPid,
+ callerApp, originatingPendingIntent,
backgroundStartPrivileges, intent, checkedOptions) == BAL_BLOCK;
}
@@ -288,11 +289,13 @@
}
}
+ String realCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
+
// Legacy behavior allows to use caller foreground state to bypass BAL restriction.
// The options here are the options passed by the sender and not those on the intent.
final BackgroundStartPrivileges balAllowedByPiSender =
PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
- checkedOptions, realCallingUid);
+ checkedOptions, realCallingUid, realCallingPackage);
final boolean logVerdictChangeByPiDefaultChange = checkedOptions == null
|| checkedOptions.getPendingIntentBackgroundActivityStartMode()
@@ -460,8 +463,11 @@
// If we are here, it means all exemptions not based on PI sender failed, so we'll block
// unless resultIfPiSenderAllowsBal is an allow and the PI sender allows BAL
- String realCallingPackage = callingUid == realCallingUid ? callingPackage :
- mService.mContext.getPackageManager().getNameForUid(realCallingUid);
+ if (realCallingPackage == null) {
+ realCallingPackage = (callingUid == realCallingUid ? callingPackage :
+ mService.mContext.getPackageManager().getNameForUid(realCallingUid))
+ + "[debugOnly]";
+ }
String stateDumpLog = " [callingPackage: " + callingPackage
+ "; callingUid: " + callingUid
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 322c11a..e33c6f0 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9254,7 +9254,6 @@
boolean shouldRestoreImeVisibility(IBinder imeTargetWindowToken) {
final Task imeTargetWindowTask;
- boolean hadRequestedShowIme = false;
synchronized (mGlobalLock) {
final WindowState imeTargetWindow = mWindowMap.get(imeTargetWindowToken);
if (imeTargetWindow == null) {
@@ -9264,14 +9263,15 @@
if (imeTargetWindowTask == null) {
return false;
}
- if (imeTargetWindow.mActivityRecord != null) {
- hadRequestedShowIme = imeTargetWindow.mActivityRecord.mLastImeShown;
+ if (imeTargetWindow.mActivityRecord != null
+ && imeTargetWindow.mActivityRecord.mLastImeShown) {
+ return true;
}
}
final TaskSnapshot snapshot = getTaskSnapshot(imeTargetWindowTask.mTaskId,
imeTargetWindowTask.mUserId, false /* isLowResolution */,
false /* restoreFromDisk */);
- return snapshot != null && snapshot.hasImeSurface() || hadRequestedShowIme;
+ return snapshot != null && snapshot.hasImeSurface();
}
@Override
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index dbd9e4b..3672820 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -721,6 +721,12 @@
}
}
+ List<String> getPackageList() {
+ synchronized (mPkgList) {
+ return new ArrayList<>(mPkgList);
+ }
+ }
+
void addActivityIfNeeded(ActivityRecord r) {
// even if we already track this activity, note down that it has been launched
setLastActivityLaunchTime(r);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index ad5f0d7..6365764 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -269,7 +269,9 @@
deliverRes = res;
break;
}
+ res.setPendingStart(true);
mHandlerThread.getThreadHandler().post(() -> {
+ res.setPendingStart(false);
synchronized (mAms) {
switch (behavior) {
case SUCCESS:
@@ -281,6 +283,10 @@
mActiveProcesses.remove(deliverRes);
mQueue.onApplicationTimeoutLocked(deliverRes);
break;
+ case KILLED_WITHOUT_NOTIFY:
+ mActiveProcesses.remove(res);
+ res.setKilled(true);
+ break;
default:
throw new UnsupportedOperationException();
}
@@ -310,6 +316,7 @@
mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
mConstants.TIMEOUT = 100;
mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
+ mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
@@ -381,6 +388,8 @@
FAIL_TIMEOUT_PREDECESSOR,
/** Process fails by immediately returning null */
FAIL_NULL,
+ /** Process is killed without reporting to BroadcastQueue */
+ KILLED_WITHOUT_NOTIFY,
}
private enum ProcessBehavior {
@@ -522,6 +531,11 @@
return info;
}
+ static BroadcastFilter withPriority(BroadcastFilter filter, int priority) {
+ filter.setPriority(priority);
+ return filter;
+ }
+
static ResolveInfo makeManifestReceiver(String packageName, String name) {
return makeManifestReceiver(packageName, name, UserHandle.USER_SYSTEM);
}
@@ -1261,6 +1275,46 @@
new ComponentName(PACKAGE_GREEN, CLASS_GREEN));
}
+ /**
+ * Verify that when BroadcastQueue doesn't get notified when a process gets killed, it
+ * doesn't get stuck.
+ */
+ @Test
+ public void testKillWithoutNotify() throws Exception {
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+ final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+
+ mNextProcessStartBehavior.set(ProcessStartBehavior.KILLED_WITHOUT_NOTIFY);
+
+ final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of(
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10),
+ withPriority(makeRegisteredReceiver(receiverBlueApp), 5),
+ withPriority(makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW), 0))));
+
+ final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(timezone, callerApp,
+ List.of(makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE))));
+
+ waitForIdle();
+ final ProcessRecord receiverGreenApp = mAms.getProcessRecordLocked(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+ getUidForPackage(PACKAGE_YELLOW));
+ final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE,
+ getUidForPackage(PACKAGE_ORANGE));
+
+ if (mImpl == Impl.MODERN) {
+ // Modern queue does not retry sending a broadcast once any broadcast delivery fails.
+ assertNull(receiverGreenApp);
+ } else {
+ verifyScheduleReceiver(times(1), receiverGreenApp, airplane);
+ }
+ verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, airplane);
+ verifyScheduleReceiver(times(1), receiverYellowApp, airplane);
+ verifyScheduleReceiver(times(1), receiverOrangeApp, timezone);
+ }
+
@Test
public void testCold_Success() throws Exception {
doCold(ProcessStartBehavior.SUCCESS);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index 154aa7d4..4268eb9 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -231,7 +231,14 @@
public void testMultiAuth_singleSensor_fingerprintSensorStartsAfterDialogAnimationCompletes()
throws Exception {
setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
- testMultiAuth_fingerprintSensorStartsAfterUINotifies();
+ testMultiAuth_fingerprintSensorStartsAfterUINotifies(true /* startFingerprintNow */);
+ }
+
+ @Test
+ public void testMultiAuth_singleSensor_fingerprintSensorDoesNotStartAfterDialogAnimationCompletes()
+ throws Exception {
+ setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+ testMultiAuth_fingerprintSensorStartsAfterUINotifies(false /* startFingerprintNow */);
}
@Test
@@ -239,10 +246,18 @@
throws Exception {
setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
- testMultiAuth_fingerprintSensorStartsAfterUINotifies();
+ testMultiAuth_fingerprintSensorStartsAfterUINotifies(true /* startFingerprintNow */);
}
- public void testMultiAuth_fingerprintSensorStartsAfterUINotifies()
+ @Test
+ public void testMultiAuth_fingerprintSensorDoesNotStartAfterDialogAnimationCompletes()
+ throws Exception {
+ setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+ setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class));
+ testMultiAuth_fingerprintSensorStartsAfterUINotifies(false /* startFingerprintNow */);
+ }
+
+ public void testMultiAuth_fingerprintSensorStartsAfterUINotifies(boolean startFingerprintNow)
throws Exception {
final long operationId = 123;
final int userId = 10;
@@ -282,13 +297,21 @@
// fingerprint sensor does not start even if all cookies are received
assertEquals(STATE_AUTH_STARTED, session.getState());
verify(mStatusBarService).showAuthenticationDialog(any(), any(), any(),
- anyBoolean(), anyBoolean(), anyInt(), anyLong(), any(), anyLong(), anyInt());
+ anyBoolean(), anyBoolean(), anyInt(), anyLong(), any(), anyLong());
// Notify AuthSession that the UI is shown. Then, fingerprint sensor should be started.
- session.onDialogAnimatedIn();
+ session.onDialogAnimatedIn(startFingerprintNow);
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
- assertEquals(BiometricSensor.STATE_AUTHENTICATING,
+ assertEquals(startFingerprintNow ? BiometricSensor.STATE_AUTHENTICATING
+ : BiometricSensor.STATE_COOKIE_RETURNED,
session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
+
+ // start fingerprint sensor if it was delayed
+ if (!startFingerprintNow) {
+ session.onStartFingerprint();
+ assertEquals(BiometricSensor.STATE_AUTHENTICATING,
+ session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState());
+ }
}
@Test
@@ -316,14 +339,14 @@
verify(impl, never()).startPreparedClient(anyInt());
// First invocation should start the client monitor.
- session.onDialogAnimatedIn();
+ session.onDialogAnimatedIn(true /* startFingerprintNow */);
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
verify(impl).startPreparedClient(anyInt());
// Subsequent invocations should not start the client monitor again.
- session.onDialogAnimatedIn();
- session.onDialogAnimatedIn();
- session.onDialogAnimatedIn();
+ session.onDialogAnimatedIn(true /* startFingerprintNow */);
+ session.onDialogAnimatedIn(false /* startFingerprintNow */);
+ session.onDialogAnimatedIn(true /* startFingerprintNow */);
assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
verify(impl, times(1)).startPreparedClient(anyInt());
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
index 520e1c8..67be376 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java
@@ -18,7 +18,7 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.Authenticators;
-import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT;
+import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;
@@ -311,8 +311,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
}
@Test
@@ -397,8 +396,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
}
@Test
@@ -516,7 +514,7 @@
assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState());
// startPreparedClient invoked
- mBiometricService.mAuthSession.onDialogAnimatedIn();
+ mBiometricService.mAuthSession.onDialogAnimatedIn(true /* startFingerprintNow */);
verify(mBiometricService.mSensors.get(0).impl)
.startPreparedClient(cookieCaptor.getValue());
@@ -530,8 +528,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
// Hardware authenticated
final byte[] HAT = generateRandomHAT();
@@ -587,8 +584,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
}
@Test
@@ -752,8 +748,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
anyString(),
- anyLong() /* requestId */,
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ anyLong() /* requestId */);
}
@Test
@@ -854,8 +849,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
}
@Test
@@ -935,8 +929,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(TEST_REQUEST_ID),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(TEST_REQUEST_ID));
}
@Test
@@ -1432,8 +1425,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(requestId),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(requestId));
// Requesting strong and credential, when credential is setup
resetReceivers();
@@ -1456,8 +1448,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(requestId),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(requestId));
// Un-downgrading the authenticator allows successful strong auth
for (BiometricSensor sensor : mBiometricService.mSensors) {
@@ -1482,8 +1473,7 @@
anyInt() /* userId */,
anyLong() /* operationId */,
eq(TEST_PACKAGE_NAME),
- eq(requestId),
- eq(BIOMETRIC_MULTI_SENSOR_DEFAULT));
+ eq(requestId));
}
@Test(expected = IllegalStateException.class)
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 2671e77..2b589bf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -944,7 +944,7 @@
anyInt(), anyInt()));
doReturn(BackgroundStartPrivileges.allowBackgroundActivityStarts(null)).when(
() -> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(
- anyObject(), anyInt()));
+ anyObject(), anyInt(), anyObject()));
runAndVerifyBackgroundActivityStartsSubtest(
"allowed_notAborted", false,
UNIMPORTANT_UID, false, PROCESS_STATE_BOUND_TOP,
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
index 3a65104..7598952 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java
@@ -84,6 +84,7 @@
import com.android.internal.app.IHotwordRecognitionStatusCallback;
import com.android.internal.infra.AndroidFuture;
import com.android.server.LocalServices;
+import com.android.server.policy.AppOpsPolicy;
import com.android.server.voiceinteraction.VoiceInteractionManagerServiceImpl.DetectorRemoteExceptionListener;
import java.io.Closeable;
@@ -742,18 +743,24 @@
void enforcePermissionsForDataDelivery() {
Binder.withCleanCallingIdentity(() -> {
synchronized (mLock) {
- int result = PermissionChecker.checkPermissionForPreflight(
- mContext, RECORD_AUDIO, /* pid */ -1, mVoiceInteractorIdentity.uid,
- mVoiceInteractorIdentity.packageName);
- if (result != PermissionChecker.PERMISSION_GRANTED) {
- throw new SecurityException(
- "Failed to obtain permission RECORD_AUDIO for identity "
- + mVoiceInteractorIdentity);
+ if (AppOpsPolicy.isHotwordDetectionServiceRequired(mContext.getPackageManager())) {
+ int result = PermissionChecker.checkPermissionForPreflight(
+ mContext, RECORD_AUDIO, /* pid */ -1, mVoiceInteractorIdentity.uid,
+ mVoiceInteractorIdentity.packageName);
+ if (result != PermissionChecker.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ "Failed to obtain permission RECORD_AUDIO for identity "
+ + mVoiceInteractorIdentity);
+ }
+ int hotwordOp = AppOpsManager.strOpToOp(
+ AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
+ mAppOpsManager.noteOpNoThrow(hotwordOp,
+ mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+ mVoiceInteractorIdentity.attributionTag, HOTWORD_DETECTION_OP_MESSAGE);
+ } else {
+ enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
+ RECORD_AUDIO, HOTWORD_DETECTION_OP_MESSAGE);
}
- int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
- mAppOpsManager.noteOpNoThrow(hotwordOp,
- mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
- mVoiceInteractorIdentity.attributionTag, HOTWORD_DETECTION_OP_MESSAGE);
enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity,
CAPTURE_AUDIO_HOTWORD, HOTWORD_DETECTION_OP_MESSAGE);
}