Merge "Override applyLayoutFeatures for ConstraintHelper" into udc-dev
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java b/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
index a9f720a..515ddc8 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
@@ -80,7 +80,7 @@
private void prepareForNextRun() {
SystemClock.sleep(COOL_OFF_PERIOD_MS);
- ShellHelper.runShellCommand("am wait-for-broadcast-idle");
+ ShellHelper.runShellCommand("am wait-for-broadcast-idle --flush-broadcast-loopers");
mStartTimeNs = System.nanoTime();
mPausedDurationNs = 0;
}
@@ -102,7 +102,7 @@
* to avoid unnecessary waiting.
*/
public void resumeTiming() {
- ShellHelper.runShellCommand("am wait-for-broadcast-idle");
+ ShellHelper.runShellCommand("am wait-for-broadcast-idle --flush-broadcast-loopers");
resumeTimer();
}
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
index 19a4766..6dba5b3 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -1541,7 +1541,8 @@
private void waitForBroadcastIdle() {
try {
- ShellHelper.runShellCommandWithTimeout("am wait-for-broadcast-idle", TIMEOUT_IN_SECOND);
+ ShellHelper.runShellCommandWithTimeout(
+ "am wait-for-broadcast-idle --flush-broadcast-loopers", TIMEOUT_IN_SECOND);
} catch (TimeoutException e) {
Log.e(TAG, "Ending waitForBroadcastIdle because it is taking too long", e);
}
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/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 776e34b..385fd50 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -24,9 +24,11 @@
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
+import android.annotation.UserIdInt;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
+import android.compat.annotation.LoggingOnly;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
@@ -49,6 +51,7 @@
import android.view.KeyEvent;
import android.view.View;
+import com.android.internal.compat.IPlatformCompat;
import com.android.internal.statusbar.AppClipsServiceConnector;
import com.android.internal.statusbar.IAddTileResultCallback;
import com.android.internal.statusbar.IStatusBarService;
@@ -170,6 +173,8 @@
public @interface Disable2Flags {}
// LINT.ThenChange(frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/DisableFlagsLogger.kt)
+ private static final String TAG = "StatusBarManager";
+
/**
* Default disable flags for setup
*
@@ -572,13 +577,13 @@
private static final long MEDIA_CONTROL_SESSION_ACTIONS = 203800354L;
/**
- * Media controls based on {@link android.app.Notification.MediaStyle} notifications will be
- * required to include a non-empty title, either in the {@link android.media.MediaMetadata} or
+ * Media controls based on {@link android.app.Notification.MediaStyle} notifications should
+ * include a non-empty title, either in the {@link android.media.MediaMetadata} or
* notification title.
*/
@ChangeId
- @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
- private static final long MEDIA_CONTROL_REQUIRES_TITLE = 274775190L;
+ @LoggingOnly
+ private static final long MEDIA_CONTROL_BLANK_TITLE = 274775190L;
@UnsupportedAppUsage
private Context mContext;
@@ -586,6 +591,9 @@
@UnsupportedAppUsage
private IBinder mToken = new Binder();
+ private final IPlatformCompat mPlatformCompat = IPlatformCompat.Stub.asInterface(
+ ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
+
@UnsupportedAppUsage
StatusBarManager(Context context) {
mContext = context;
@@ -597,7 +605,7 @@
mService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
if (mService == null) {
- Slog.w("StatusBarManager", "warning: no STATUS_BAR_SERVICE");
+ Slog.w(TAG, "warning: no STATUS_BAR_SERVICE");
}
}
return mService;
@@ -1226,18 +1234,22 @@
}
/**
- * Checks whether the given package must include a non-empty title for its media controls.
+ * Log that the given package has posted media controls with a blank title
*
* @param packageName App posting media controls
- * @param user Current user handle
- * @return true if the app is required to provide a non-empty title
+ * @param userId Current user ID
+ * @throws RuntimeException if there is an error reporting the change
*
* @hide
*/
- @RequiresPermission(allOf = {android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
- android.Manifest.permission.LOG_COMPAT_CHANGE})
- public static boolean isMediaTitleRequiredForApp(String packageName, UserHandle user) {
- return CompatChanges.isChangeEnabled(MEDIA_CONTROL_REQUIRES_TITLE, packageName, user);
+ public void logBlankMediaTitle(String packageName, @UserIdInt int userId)
+ throws RuntimeException {
+ try {
+ mPlatformCompat.reportChangeByPackageName(MEDIA_CONTROL_BLANK_TITLE, packageName,
+ userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
/**
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/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index b601275..3ffbe1d 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -1,223 +1,170 @@
{
- "imports": [
- {
- "path": "frameworks/base/core/tests/coretests/src/android/content/pm"
- },
- {
- "path": "frameworks/base/services/tests/PackageManagerServiceTests"
- },
- {
- "path": "frameworks/base/services/tests/PackageManager"
- },
- {
- "path": "frameworks/base/services/tests/PackageManagerComponentOverrideTests"
- },
- {
- "path": "frameworks/base/services/tests/servicestests/src/com/android/server/pm"
- },
- {
- "path": "cts/tests/tests/packageinstaller"
- },
- {
- "path": "cts/hostsidetests/stagedinstall"
- },
- {
- "path": "cts/hostsidetests/packagemanager"
- },
- {
- "path": "cts/hostsidetests/os/test_mappings/packagemanager"
- },
- {
- "path": "cts/hostsidetests/appsearch"
- },
- {
- "path": "system/apex/tests"
- },
- {
- "path": "cts/tests/tests/content/pm/SecureFrp"
- }
- ],
- "presubmit": [
- {
- "name": "CtsInstantAppTests",
- "file_patterns": ["(/|^)InstantApp[^/]*"]
- },
- {
- "name": "CarrierAppIntegrationTestCases"
- },
- {
- "name": "ApkVerityTest"
- },
- {
- "name": "CtsSilentUpdateHostTestCases"
- },
- {
- "name": "CtsSuspendAppsTestCases"
- },
- {
- "name": "CtsAppFgsTestCases",
- "file_patterns": ["(/|^)ServiceInfo[^/]*"],
- "options": [
+ "imports":[
{
- "include-annotation": "android.platform.test.annotations.Presubmit"
+ "path":"frameworks/base/core/tests/coretests/src/android/content/pm"
},
{
- "exclude-annotation": "androidx.test.filters.LargeTest"
+ "path":"frameworks/base/services/tests/PackageManagerServiceTests"
},
{
- "exclude-annotation": "androidx.test.filters.FlakyTest"
+ "path":"frameworks/base/services/tests/PackageManager"
+ },
+ {
+ "path":"frameworks/base/services/tests/PackageManagerComponentOverrideTests"
+ },
+ {
+ "path":"frameworks/base/services/tests/servicestests/src/com/android/server/pm"
+ },
+ {
+ "path":"cts/tests/tests/packageinstaller"
+ },
+ {
+ "path":"cts/hostsidetests/stagedinstall"
+ },
+ {
+ "path":"cts/hostsidetests/packagemanager"
+ },
+ {
+ "path":"cts/hostsidetests/os/test_mappings/packagemanager"
+ },
+ {
+ "path":"cts/hostsidetests/appsearch"
+ },
+ {
+ "path":"system/apex/tests"
+ },
+ {
+ "path":"cts/tests/tests/content/pm/SecureFrp"
}
- ]
- },
- {
- "name": "CtsShortFgsTestCases",
- "file_patterns": ["(/|^)ServiceInfo[^/]*"],
- "options": [
+ ],
+ "presubmit":[
{
- "include-annotation": "android.platform.test.annotations.Presubmit"
+ "name":"CtsInstantAppTests",
+ "file_patterns":[
+ "(/|^)InstantApp[^/]*"
+ ]
},
{
- "exclude-annotation": "androidx.test.filters.LargeTest"
+ "name":"CarrierAppIntegrationTestCases"
},
{
- "exclude-annotation": "androidx.test.filters.FlakyTest"
+ "name":"ApkVerityTest"
+ },
+ {
+ "name":"CtsSilentUpdateHostTestCases"
+ },
+ {
+ "name":"CtsSuspendAppsTestCases"
+ },
+ {
+ "name":"CtsAppFgsTestCases",
+ "file_patterns":[
+ "(/|^)ServiceInfo[^/]*"
+ ],
+ "options":[
+ {
+ "include-annotation":"android.platform.test.annotations.Presubmit"
+ },
+ {
+ "exclude-annotation":"androidx.test.filters.LargeTest"
+ },
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ }
+ ]
+ },
+ {
+ "name":"CtsShortFgsTestCases",
+ "file_patterns":[
+ "(/|^)ServiceInfo[^/]*"
+ ],
+ "options":[
+ {
+ "include-annotation":"android.platform.test.annotations.Presubmit"
+ },
+ {
+ "exclude-annotation":"androidx.test.filters.LargeTest"
+ },
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ }
+ ]
+ },
+ {
+ "name":"CtsIncrementalInstallHostTestCases",
+ "options":[
+ {
+ "include-filter":"android.incrementalinstall.cts.IncrementalFeatureTest"
+ }
+ ]
}
- ]
- },
- {
- "name": "CtsIncrementalInstallHostTestCases",
- "options": [
+ ],
+ "presubmit-large":[
{
- "include-filter": "android.incrementalinstall.cts.IncrementalFeatureTest"
- }
- ]
- }
- ],
- "presubmit-large": [
- {
- "name": "CtsContentTestCases",
- "options": [
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
+ "name":"CtsContentTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ },
+ {
+ "include-filter":"android.content.pm.cts"
+ }
+ ]
},
{
- "exclude-annotation": "org.junit.Ignore"
+ "name":"CtsUsesNativeLibraryTest",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
},
{
- "include-filter": "android.content.pm.cts"
- }
- ]
- },
- {
- "name": "CtsUsesNativeLibraryTest",
- "options": [
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
+ "name":"CtsSuspendAppsPermissionTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
},
{
- "exclude-annotation": "org.junit.Ignore"
+ "name":"CtsAppSecurityHostTestCases",
+ "options":[
+ {
+ "include-annotation":"android.platform.test.annotations.Presubmit"
+ },
+ {
+ "exclude-annotation":"android.platform.test.annotations.Postsubmit"
+ },
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
}
- ]
- },
- {
- "name": "CtsSuspendAppsPermissionTestCases",
- "options": [
+ ],
+ "postsubmit":[
{
- "exclude-annotation": "androidx.test.filters.FlakyTest"
+ "name":"CtsAppSecurityHostTestCases",
+ "options":[
+ {
+ "include-filter":"android.appsecurity.cts.AppSecurityTests#testPermissionDiffCert"
+ }
+ ]
},
{
- "exclude-annotation": "org.junit.Ignore"
+ "name":"CtsInstallHostTestCases"
}
- ]
- },
- {
- "name": "CtsAppSecurityHostTestCases",
- "options": [
- {
- "include-annotation": "android.platform.test.annotations.Presubmit"
- },
- {
- "exclude-annotation": "android.platform.test.annotations.Postsubmit"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- }
- ]
- }
- ],
- "postsubmit": [
- {
- "name": "CtsAppSecurityHostTestCases",
- "options": [
- {
- "include-filter": "android.appsecurity.cts.AppSecurityTests#testPermissionDiffCert"
- }
- ]
- },
- {
- "name": "CtsInstallHostTestCases"
- }
- ],
- "staged-platinum-postsubmit": [
- {
- "name": "CtsIncrementalInstallHostTestCases"
- },
- {
- "name": "CtsAppSecurityHostTestCases",
- "options": [
- {
- "include-filter": "android.appsecurity.cts.SplitTests"
- },
- {
- "include-filter": "android.appsecurity.cts.EphemeralTest"
- }
- ]
- },
- {
- "name": "CtsContentTestCases",
- "options": [
- {
- "include-filter": "android.content.cts.IntentFilterTest"
- }
- ]
- }
- ],
- "platinum-postsubmit": [
- {
- "name": "CtsIncrementalInstallHostTestCases",
- "options": [
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- }
- ]
- },
- {
- "name": "CtsAppSecurityHostTestCases",
- "options": [
- {
- "include-filter": "android.appsecurity.cts.SplitTests"
- },
- {
- "include-filter": "android.appsecurity.cts.EphemeralTest"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- }
- ]
- },
- {
- "name": "CtsContentTestCases",
- "options":[
- {
- "include-filter": "android.content.cts.IntentFilterTest"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- }
- ]
- }
- ]
-}
+ ]
+}
\ No newline at end of file
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/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java
index 6910501..78388ef 100644
--- a/core/java/android/inputmethodservice/NavigationBarController.java
+++ b/core/java/android/inputmethodservice/NavigationBarController.java
@@ -246,8 +246,7 @@
@Override
public void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets,
@NonNull ViewTreeObserver.InternalInsetsInfo dest) {
- if (!mImeDrawsImeNavBar || mNavigationBarFrame == null
- || mService.isExtractViewShown()) {
+ if (!mImeDrawsImeNavBar || mNavigationBarFrame == null) {
return;
}
@@ -255,53 +254,58 @@
if (systemInsets != null) {
final Window window = mService.mWindow.getWindow();
final View decor = window.getDecorView();
- Region touchableRegion = null;
- final View inputFrame = mService.mInputFrame;
- switch (originalInsets.touchableInsets) {
- case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME:
- if (inputFrame.getVisibility() == View.VISIBLE) {
- inputFrame.getLocationInWindow(mTempPos);
- mTempRect.set(mTempPos[0], mTempPos[1],
- mTempPos[0] + inputFrame.getWidth(),
- mTempPos[1] + inputFrame.getHeight());
- touchableRegion = new Region(mTempRect);
- }
- break;
- case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT:
- if (inputFrame.getVisibility() == View.VISIBLE) {
- inputFrame.getLocationInWindow(mTempPos);
- mTempRect.set(mTempPos[0], originalInsets.contentTopInsets,
- mTempPos[0] + inputFrame.getWidth() ,
- mTempPos[1] + inputFrame.getHeight());
- touchableRegion = new Region(mTempRect);
- }
- break;
- case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE:
- if (inputFrame.getVisibility() == View.VISIBLE) {
- inputFrame.getLocationInWindow(mTempPos);
- mTempRect.set(mTempPos[0], originalInsets.visibleTopInsets,
- mTempPos[0] + inputFrame.getWidth(),
- mTempPos[1] + inputFrame.getHeight());
- touchableRegion = new Region(mTempRect);
- }
- break;
- case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION:
- touchableRegion = new Region();
- touchableRegion.set(originalInsets.touchableRegion);
- break;
- }
- // Hereafter "mTempRect" means a navigation bar rect.
- mTempRect.set(decor.getLeft(), decor.getBottom() - systemInsets.bottom,
- decor.getRight(), decor.getBottom());
- if (touchableRegion == null) {
- touchableRegion = new Region(mTempRect);
- } else {
- touchableRegion.union(mTempRect);
- }
- dest.touchableRegion.set(touchableRegion);
- dest.setTouchableInsets(
- ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ // If the extract view is shown, everything is touchable, so no need to update
+ // touchable insets, but we still update normal insets below.
+ if (!mService.isExtractViewShown()) {
+ Region touchableRegion = null;
+ final View inputFrame = mService.mInputFrame;
+ switch (originalInsets.touchableInsets) {
+ case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME:
+ if (inputFrame.getVisibility() == View.VISIBLE) {
+ inputFrame.getLocationInWindow(mTempPos);
+ mTempRect.set(mTempPos[0], mTempPos[1],
+ mTempPos[0] + inputFrame.getWidth(),
+ mTempPos[1] + inputFrame.getHeight());
+ touchableRegion = new Region(mTempRect);
+ }
+ break;
+ case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT:
+ if (inputFrame.getVisibility() == View.VISIBLE) {
+ inputFrame.getLocationInWindow(mTempPos);
+ mTempRect.set(mTempPos[0], originalInsets.contentTopInsets,
+ mTempPos[0] + inputFrame.getWidth(),
+ mTempPos[1] + inputFrame.getHeight());
+ touchableRegion = new Region(mTempRect);
+ }
+ break;
+ case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE:
+ if (inputFrame.getVisibility() == View.VISIBLE) {
+ inputFrame.getLocationInWindow(mTempPos);
+ mTempRect.set(mTempPos[0], originalInsets.visibleTopInsets,
+ mTempPos[0] + inputFrame.getWidth(),
+ mTempPos[1] + inputFrame.getHeight());
+ touchableRegion = new Region(mTempRect);
+ }
+ break;
+ case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION:
+ touchableRegion = new Region();
+ touchableRegion.set(originalInsets.touchableRegion);
+ break;
+ }
+ // Hereafter "mTempRect" means a navigation bar rect.
+ mTempRect.set(decor.getLeft(), decor.getBottom() - systemInsets.bottom,
+ decor.getRight(), decor.getBottom());
+ if (touchableRegion == null) {
+ touchableRegion = new Region(mTempRect);
+ } else {
+ touchableRegion.union(mTempRect);
+ }
+
+ dest.touchableRegion.set(touchableRegion);
+ dest.setTouchableInsets(
+ ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ }
// TODO(b/215443343): See if we can use View#OnLayoutChangeListener().
// TODO(b/215443343): See if we can replace DecorView#mNavigationColorViewState.view
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/view/SurfaceControlRegistry.java b/core/java/android/view/SurfaceControlRegistry.java
index 095189a..67ac811 100644
--- a/core/java/android/view/SurfaceControlRegistry.java
+++ b/core/java/android/view/SurfaceControlRegistry.java
@@ -62,7 +62,6 @@
private static class DefaultReporter implements Reporter {
public void onMaxLayersExceeded(WeakHashMap<SurfaceControl, Long> surfaceControls,
int limit, PrintWriter pw) {
- final int size = Math.min(surfaceControls.size(), limit);
final long now = SystemClock.elapsedRealtime();
final ArrayList<Map.Entry<SurfaceControl, Long>> entries = new ArrayList<>();
for (Map.Entry<SurfaceControl, Long> entry : surfaceControls.entrySet()) {
@@ -71,6 +70,7 @@
// Sort entries by time registered when dumping
// TODO: Or should it sort by name?
entries.sort((o1, o2) -> (int) (o1.getValue() - o2.getValue()));
+ final int size = Math.min(entries.size(), limit);
pw.println("SurfaceControlRegistry");
pw.println("----------------------");
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 441636d..f1cde3b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -7734,13 +7734,14 @@
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
- final ListenerInfo li = mListenerInfo;
+ final OnLongClickListener listener =
+ mListenerInfo == null ? null : mListenerInfo.mOnLongClickListener;
boolean shouldPerformHapticFeedback = true;
- if (li != null && li.mOnLongClickListener != null) {
- handled = li.mOnLongClickListener.onLongClick(View.this);
+ if (listener != null) {
+ handled = listener.onLongClick(View.this);
if (handled) {
- shouldPerformHapticFeedback =
- li.mOnLongClickListener.onLongClickUseDefaultHapticFeedback(View.this);
+ shouldPerformHapticFeedback = listener.onLongClickUseDefaultHapticFeedback(
+ View.this);
}
}
if (!handled) {
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 34e6e49..5525336 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -2598,6 +2598,11 @@
public int getActionTag() {
return VIEW_GROUP_ACTION_ADD_TAG;
}
+
+ @Override
+ public final void visitUris(@NonNull Consumer<Uri> visitor) {
+ mNestedViews.visitUris(visitor);
+ }
}
/**
diff --git a/core/java/android/window/TaskSnapshot.java b/core/java/android/window/TaskSnapshot.java
index b7bb608..41b6d31 100644
--- a/core/java/android/window/TaskSnapshot.java
+++ b/core/java/android/window/TaskSnapshot.java
@@ -28,6 +28,7 @@
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.SystemClock;
import android.view.Surface;
import android.view.WindowInsetsController;
@@ -38,6 +39,9 @@
public class TaskSnapshot implements Parcelable {
// Identifier of this snapshot
private final long mId;
+ // The elapsed real time (in nanoseconds) when this snapshot was captured, not intended for use outside the
+ // process in which the snapshot was taken (ie. this is not parceled)
+ private final long mCaptureTime;
// Top activity in task when snapshot was taken
private final ComponentName mTopActivityComponent;
private final HardwareBuffer mSnapshot;
@@ -65,7 +69,7 @@
// Must be one of the named color spaces, otherwise, always use SRGB color space.
private final ColorSpace mColorSpace;
- public TaskSnapshot(long id,
+ public TaskSnapshot(long id, long captureTime,
@NonNull ComponentName topActivityComponent, HardwareBuffer snapshot,
@NonNull ColorSpace colorSpace, int orientation, int rotation, Point taskSize,
Rect contentInsets, Rect letterboxInsets, boolean isLowResolution,
@@ -73,6 +77,7 @@
@WindowInsetsController.Appearance int appearance, boolean isTranslucent,
boolean hasImeSurface) {
mId = id;
+ mCaptureTime = captureTime;
mTopActivityComponent = topActivityComponent;
mSnapshot = snapshot;
mColorSpace = colorSpace.getId() < 0
@@ -92,6 +97,7 @@
private TaskSnapshot(Parcel source) {
mId = source.readLong();
+ mCaptureTime = SystemClock.elapsedRealtimeNanos();
mTopActivityComponent = ComponentName.readFromParcel(source);
mSnapshot = source.readTypedObject(HardwareBuffer.CREATOR);
int colorSpaceId = source.readInt();
@@ -119,6 +125,14 @@
}
/**
+ * @return The elapsed real time (in nanoseconds) when this snapshot was captured. This time is
+ * only valid in the process where this snapshot was taken.
+ */
+ public long getCaptureTime() {
+ return mCaptureTime;
+ }
+
+ /**
* @return The top activity component for the task at the point this snapshot was taken.
*/
public ComponentName getTopActivityComponent() {
@@ -268,6 +282,7 @@
final int height = mSnapshot != null ? mSnapshot.getHeight() : 0;
return "TaskSnapshot{"
+ " mId=" + mId
+ + " mCaptureTime=" + mCaptureTime
+ " mTopActivityComponent=" + mTopActivityComponent.flattenToShortString()
+ " mSnapshot=" + mSnapshot + " (" + width + "x" + height + ")"
+ " mColorSpace=" + mColorSpace.toString()
@@ -296,6 +311,7 @@
/** Builder for a {@link TaskSnapshot} object */
public static final class Builder {
private long mId;
+ private long mCaptureTime;
private ComponentName mTopActivity;
private HardwareBuffer mSnapshot;
private ColorSpace mColorSpace;
@@ -317,6 +333,11 @@
return this;
}
+ public Builder setCaptureTime(long captureTime) {
+ mCaptureTime = captureTime;
+ return this;
+ }
+
public Builder setTopActivityComponent(ComponentName name) {
mTopActivity = name;
return this;
@@ -400,6 +421,7 @@
public TaskSnapshot build() {
return new TaskSnapshot(
mId,
+ mCaptureTime,
mTopActivity,
mSnapshot,
mColorSpace,
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/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index f277635..116c301c 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -367,28 +367,42 @@
* using a single static object.
*/
@VisibleForTesting
+ @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
public void startListeningForLatencyTrackerConfigChanges() {
final Context context = ActivityThread.currentApplication();
- if (context != null
- && context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) == PERMISSION_GRANTED) {
- // Post initialization to the background in case we're running on the main thread.
- BackgroundThread.getHandler().post(() -> this.updateProperties(
- DeviceConfig.getProperties(NAMESPACE_LATENCY_TRACKER)));
- DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_LATENCY_TRACKER,
- BackgroundThread.getExecutor(), mOnPropertiesChangedListener);
- } else {
+ if (context == null) {
if (DEBUG) {
- if (context == null) {
- Log.d(TAG, "No application for " + ActivityThread.currentActivityThread());
- } else {
- synchronized (mLock) {
- Log.d(TAG, "Initialized the LatencyTracker."
- + " (No READ_DEVICE_CONFIG permission to change configs)"
- + " enabled=" + mEnabled + ", package=" + context.getPackageName());
- }
+ Log.d(TAG, "No application for package: " + ActivityThread.currentPackageName());
+ }
+ return;
+ }
+ if (context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) != PERMISSION_GRANTED) {
+ if (DEBUG) {
+ synchronized (mLock) {
+ Log.d(TAG, "Initialized the LatencyTracker."
+ + " (No READ_DEVICE_CONFIG permission to change configs)"
+ + " enabled=" + mEnabled + ", package=" + context.getPackageName());
}
}
+ return;
}
+
+ // Post initialization to the background in case we're running on the main thread.
+ BackgroundThread.getHandler().post(() -> {
+ try {
+ this.updateProperties(
+ DeviceConfig.getProperties(NAMESPACE_LATENCY_TRACKER));
+ DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_LATENCY_TRACKER,
+ BackgroundThread.getExecutor(), mOnPropertiesChangedListener);
+ } catch (SecurityException ex) {
+ // In case of running tests that the main thread passes the check,
+ // but the background thread doesn't have necessary permissions.
+ // Swallow it since it's ok to ignore device config changes in the tests.
+ Log.d(TAG, "Can't get properties: READ_DEVICE_CONFIG granted="
+ + context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG)
+ + ", package=" + context.getPackageName());
+ }
+ });
}
/**
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/strings.xml b/core/res/res/values/strings.xml
index 91fbf6b..fb3acbe 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5410,6 +5410,9 @@
<!-- Title for button to see application detail in app store which it came from - it may allow user to update to newer version. [CHAR LIMIT=50] -->
<string name="deprecated_target_sdk_app_store">Check for update</string>
+ <!-- Message displayed in dialog when app is 32 bit on a 64 bit system. [CHAR LIMIT=NONE] -->
+ <string name="deprecated_abi_message">This app isn\'t compatible with the latest version of Android. Check for an update or contact the app\'s developer.</string>
+
<!-- Notification title shown when new SMS/MMS is received while the device is locked [CHAR LIMIT=NONE] -->
<string name="new_sms_notification_title">You have new messages</string>
<!-- Notification content shown when new SMS/MMS is received while the device is locked [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f35e32b..6ab671a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3156,6 +3156,8 @@
<java-symbol type="string" name="deprecated_target_sdk_message" />
<java-symbol type="string" name="deprecated_target_sdk_app_store" />
+ <java-symbol type="string" name="deprecated_abi_message" />
+
<!-- New SMS notification while phone is locked. -->
<java-symbol type="string" name="new_sms_notification_title" />
<java-symbol type="string" name="new_sms_notification_content" />
@@ -5129,4 +5131,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/core/tests/coretests/src/android/content/res/TEST_MAPPING b/core/tests/coretests/src/android/content/res/TEST_MAPPING
index ab14950..4ea6e40 100644
--- a/core/tests/coretests/src/android/content/res/TEST_MAPPING
+++ b/core/tests/coretests/src/android/content/res/TEST_MAPPING
@@ -39,18 +39,5 @@
}
]
}
- ],
- "ironwood-postsubmit": [
- {
- "name": "FrameworksCoreTests",
- "options":[
- {
- "include-annotation": "android.platform.test.annotations.IwTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- }
- ]
- }
]
}
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
index 4672226..7879801 100644
--- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java
+++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
@@ -716,6 +716,30 @@
}
@Test
+ public void visitUris_nestedViews() {
+ final RemoteViews outer = new RemoteViews(mPackage, R.layout.remote_views_test);
+
+ final RemoteViews inner = new RemoteViews(mPackage, 33);
+ final Uri imageUriI = Uri.parse("content://inner/image");
+ final Icon icon1 = Icon.createWithContentUri("content://inner/icon1");
+ final Icon icon2 = Icon.createWithContentUri("content://inner/icon2");
+ final Icon icon3 = Icon.createWithContentUri("content://inner/icon3");
+ final Icon icon4 = Icon.createWithContentUri("content://inner/icon4");
+ inner.setImageViewUri(R.id.image, imageUriI);
+ inner.setTextViewCompoundDrawables(R.id.text, icon1, icon2, icon3, icon4);
+
+ outer.addView(R.id.layout, inner);
+
+ Consumer<Uri> visitor = (Consumer<Uri>) spy(Consumer.class);
+ outer.visitUris(visitor);
+ verify(visitor, times(1)).accept(eq(imageUriI));
+ verify(visitor, times(1)).accept(eq(icon1.getUri()));
+ verify(visitor, times(1)).accept(eq(icon2.getUri()));
+ verify(visitor, times(1)).accept(eq(icon3.getUri()));
+ verify(visitor, times(1)).accept(eq(icon4.getUri()));
+ }
+
+ @Test
public void visitUris_separateOrientation() {
final RemoteViews landscape = new RemoteViews(mPackage, R.layout.remote_views_test);
final Uri imageUriL = Uri.parse("content://landscape/image");
diff --git a/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java b/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java
index 281d677..6764ac8 100644
--- a/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java
+++ b/core/tests/coretests/src/android/window/SnapshotDrawerUtilsTest.java
@@ -88,6 +88,7 @@
1, HardwareBuffer.USAGE_CPU_READ_RARELY);
return new TaskSnapshot(
System.currentTimeMillis(),
+ 0 /* captureTime */,
new ComponentName("", ""), buffer,
ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */,
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 4b4e722..e4defcf 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1135,6 +1135,12 @@
"group": "WM_DEBUG_RECENTS_ANIMATIONS",
"at": "com\/android\/server\/wm\/RecentsAnimation.java"
},
+ "-1060529098": {
+ "message": " Skipping post-transition snapshot for task %d",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/Transition.java"
+ },
"-1060365734": {
"message": "Attempted to add QS dialog window with bad token %s. Aborting.",
"level": "WARN",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TEST_MAPPING b/libs/WindowManager/Shell/src/com/android/wm/shell/TEST_MAPPING
deleted file mode 100644
index 8dd1369..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/TEST_MAPPING
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "ironwood-postsubmit": [
- {
- "name": "WMShellFlickerTests",
- "options": [
- {
- "include-annotation": "android.platform.test.annotations.IwTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- }
- ]
- }
- ]
-}
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/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 91c7cc0..68fea41 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -2518,6 +2518,7 @@
mExpandedAnimationController.expandFromStack(() -> {
updatePointerPosition(false /* forIme */);
afterExpandedViewAnimation();
+ mExpandedViewContainer.setVisibility(VISIBLE);
mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
} /* after */);
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
index ac6e4c2..53683c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
@@ -54,14 +54,6 @@
DevicePostureController.OnDevicePostureChangedListener,
DisplayController.OnDisplaysChangedListener {
/**
- * When {@code true}, floating windows like PiP would auto move to the position
- * specified by {@link #PREFER_TOP_HALF_IN_TABLETOP} when in tabletop mode.
- */
- private static final boolean ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP =
- SystemProperties.getBoolean(
- "persist.wm.debug.enable_move_floating_window_in_tabletop", true);
-
- /**
* Prefer the {@link #PREFERRED_TABLETOP_HALF_TOP} if this flag is enabled,
* {@link #PREFERRED_TABLETOP_HALF_BOTTOM} otherwise.
* See also {@link #getPreferredHalfInTabletopMode()}.
@@ -162,14 +154,6 @@
}
}
- /**
- * @return {@code true} if floating windows like PiP would auto move to the position
- * specified by {@link #getPreferredHalfInTabletopMode()} when in tabletop mode.
- */
- public boolean enableMoveFloatingWindowInTabletop() {
- return ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP;
- }
-
/** @return Preferred half for floating windows like PiP when in tabletop mode. */
@PreferredTabletopHalf
public int getPreferredHalfInTabletopMode() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index 753dfa7..a9ccdf6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -293,6 +293,9 @@
}
if (mResizingIconView == null) {
+ if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+ animFinishedCallback.accept(false);
+ }
return;
}
@@ -311,6 +314,9 @@
releaseDecor(finishT);
finishT.apply();
finishT.close();
+ if (mRunningAnimationCount == 0 && animFinishedCallback != null) {
+ animFinishedCallback.accept(true);
+ }
}
});
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
index 65a12d6..2590cab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMediaController.java
@@ -252,13 +252,16 @@
// It can be removed when min_sdk of the app is set to 31 or greater.
@SuppressLint("NewApi")
private List<RemoteAction> getMediaActions() {
- if (mMediaController == null || mMediaController.getPlaybackState() == null) {
+ // Cache the PlaybackState since it's a Binder call.
+ final PlaybackState playbackState;
+ if (mMediaController == null
+ || (playbackState = mMediaController.getPlaybackState()) == null) {
return Collections.emptyList();
}
ArrayList<RemoteAction> mediaActions = new ArrayList<>();
- boolean isPlaying = mMediaController.getPlaybackState().isActive();
- long actions = mMediaController.getPlaybackState().getActions();
+ boolean isPlaying = playbackState.isActive();
+ long actions = playbackState.getActions();
// Prev action
mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index bfc1fb9..3c7ce3c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -249,11 +249,6 @@
finishTransaction);
}
- // Fade in the fadeout PIP when the fixed rotation is finished.
- if (mPipTransitionState.isInPip() && !mInFixedRotation && mHasFadeOut) {
- fadeExistingPip(true /* show */);
- }
-
return false;
}
@@ -1056,6 +1051,12 @@
.crop(finishTransaction, leash, destBounds)
.round(finishTransaction, leash, isInPip)
.shadow(finishTransaction, leash, isInPip);
+ // Make sure the PiP keeps invisible if it was faded out. If it needs to fade in, that will
+ // be handled by onFixedRotationFinished().
+ if (isInPip && mHasFadeOut) {
+ startTransaction.setAlpha(leash, 0f);
+ finishTransaction.setAlpha(leash, 0f);
+ }
}
/** Hides and shows the existing PIP during fixed rotation transition of other activities. */
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/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 63181da..6a861ce 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -677,7 +677,6 @@
});
mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> {
- if (!mTabletopModeController.enableMoveFloatingWindowInTabletop()) return;
final String tag = "tabletop-mode";
if (!isInTabletopMode) {
mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 5c9709c..f35eda6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -371,7 +371,8 @@
* Find the background task that match the given component.
*/
@Nullable
- public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName) {
+ public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName,
+ int userId) {
if (componentName == null) {
return null;
}
@@ -383,7 +384,7 @@
if (task.isVisible) {
continue;
}
- if (componentName.equals(task.baseIntent.getComponent())) {
+ if (componentName.equals(task.baseIntent.getComponent()) && userId == task.userId) {
return task;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index a9ad3c9..c286959 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -264,21 +264,18 @@
void cancel(String reason) {
// restoring (to-home = false) involves submitting more WM changes, so by default, use
// toHome = true when canceling.
- cancel(true /* toHome */, reason);
+ cancel(true /* toHome */, false /* withScreenshots */, reason);
}
- void cancel(boolean toHome, String reason) {
+ void cancel(boolean toHome, boolean withScreenshots, String reason) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.cancel: toHome=%b reason=%s",
mInstanceId, toHome, reason);
if (mListener != null) {
- try {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
- "[%d] RecentsController.cancel: calling onAnimationCanceled",
- mInstanceId);
- mListener.onAnimationCanceled(null, null);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error canceling recents animation", e);
+ if (withScreenshots) {
+ sendCancelWithSnapshots();
+ } else {
+ sendCancel(null, null);
}
}
if (mFinishCB != null) {
@@ -300,24 +297,34 @@
snapshots = new TaskSnapshot[mPausingTasks.size()];
try {
for (int i = 0; i < mPausingTasks.size(); ++i) {
+ TaskState state = mPausingTasks.get(0);
snapshots[i] = ActivityTaskManager.getService().takeTaskSnapshot(
- mPausingTasks.get(0).mTaskInfo.taskId, false /* updateCache */);
+ state.mTaskInfo.taskId, true /* updateCache */);
}
} catch (RemoteException e) {
taskIds = null;
snapshots = null;
}
}
+ return sendCancel(taskIds, snapshots);
+ }
+
+ /**
+ * Sends a cancel message to the recents animation.
+ */
+ private boolean sendCancel(@Nullable int[] taskIds,
+ @Nullable TaskSnapshot[] taskSnapshots) {
try {
+ final String cancelDetails = taskSnapshots != null ? " with snapshots" : "";
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
- "[%d] RecentsController.cancel: calling onAnimationCanceled with snapshots",
- mInstanceId);
- mListener.onAnimationCanceled(taskIds, snapshots);
+ "[%d] RecentsController.cancel: calling onAnimationCanceled %s",
+ mInstanceId, cancelDetails);
+ mListener.onAnimationCanceled(taskIds, taskSnapshots);
+ return true;
} catch (RemoteException e) {
Slog.e(TAG, "Error canceling recents animation", e);
return false;
}
- return true;
}
void cleanUp() {
@@ -519,7 +526,7 @@
// Finish recents animation if the display is changed, so the default
// transition handler can play the animation such as rotation effect.
if (change.hasFlags(TransitionInfo.FLAG_IS_DISPLAY)) {
- cancel(mWillFinishToHome, "display change");
+ cancel(mWillFinishToHome, true /* withScreenshots */, "display change");
return;
}
// Don't consider order-only changes as changing apps.
@@ -633,7 +640,7 @@
+ foundRecentsClosing);
if (foundRecentsClosing) {
mWillFinishToHome = false;
- cancel(false /* toHome */, "didn't merge");
+ cancel(false /* toHome */, false /* withScreenshots */, "didn't merge");
}
return;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 34701f1..ea33a1f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -723,7 +723,7 @@
// in the background with priority.
final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
.map(recentTasks -> recentTasks.findTaskInBackground(
- intent.getIntent().getComponent()))
+ intent.getIntent().getComponent(), userId1))
.orElse(null);
if (taskInfo != null) {
startTask(taskInfo.taskId, position, options);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 14ea86a..d2b0e28 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -87,6 +87,14 @@
mStageCoordinator = stageCoordinator;
}
+ private void initTransition(@NonNull IBinder transition,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ mAnimatingTransition = transition;
+ mFinishTransaction = finishTransaction;
+ mFinishCallback = finishCallback;
+ }
+
/** Play animation for enter transition or dismiss transition. */
void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@@ -94,9 +102,7 @@
@NonNull Transitions.TransitionFinishCallback finishCallback,
@NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot,
@NonNull WindowContainerToken topRoot) {
- mFinishCallback = finishCallback;
- mAnimatingTransition = transition;
- mFinishTransaction = finishTransaction;
+ initTransition(transition, finishTransaction, finishCallback);
final TransitSession pendingTransition = getPendingTransition(transition);
if (pendingTransition != null) {
@@ -220,6 +226,45 @@
onFinish(null /* wct */, null /* wctCB */);
}
+ /** Play animation for drag divider dismiss transition. */
+ void playDragDismissAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback,
+ @NonNull WindowContainerToken toTopRoot, @NonNull SplitDecorManager toTopDecor,
+ @NonNull WindowContainerToken topRoot) {
+ initTransition(transition, finishTransaction, finishCallback);
+
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ final SurfaceControl leash = change.getLeash();
+
+ if (toTopRoot.equals(change.getContainer())) {
+ startTransaction.setAlpha(leash, 1.f);
+ startTransaction.show(leash);
+
+ ValueAnimator va = new ValueAnimator();
+ mAnimations.add(va);
+
+ toTopDecor.onResized(startTransaction, animated -> {
+ mAnimations.remove(va);
+ if (animated) {
+ mTransitions.getMainExecutor().execute(() -> {
+ onFinish(null /* wct */, null /* wctCB */);
+ });
+ }
+ });
+ } else if (topRoot.equals(change.getContainer())) {
+ // Ensure it on top of all changes in transition.
+ startTransaction.setLayer(leash, Integer.MAX_VALUE);
+ startTransaction.setAlpha(leash, 1.f);
+ startTransaction.show(leash);
+ }
+ }
+ startTransaction.apply();
+ onFinish(null /* wct */, null /* wctCB */);
+ }
+
/** Play animation for resize transition. */
void playResizeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@@ -227,9 +272,7 @@
@NonNull Transitions.TransitionFinishCallback finishCallback,
@NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot,
@NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) {
- mFinishCallback = finishCallback;
- mAnimatingTransition = transition;
- mFinishTransaction = finishTransaction;
+ initTransition(transition, finishTransaction, finishCallback);
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index bf20567..e0fffff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -1328,8 +1328,6 @@
mIsExiting = true;
childrenToTop.resetBounds(wct);
wct.reorder(childrenToTop.mRootTaskInfo.token, true);
- wct.setSmallestScreenWidthDp(childrenToTop.mRootTaskInfo.token,
- SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
}
wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
false /* reparentLeafTaskIfRelaunch */);
@@ -1517,6 +1515,10 @@
void finishEnterSplitScreen(SurfaceControl.Transaction t) {
mSplitLayout.update(t);
+ mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash,
+ getMainStageBounds());
+ mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash,
+ getSideStageBounds());
setDividerVisibility(true, t);
// Ensure divider surface are re-parented back into the hierarchy at the end of the
// transition. See Transition#buildFinishTransaction for more detail.
@@ -1989,13 +1991,15 @@
final boolean mainStageToTop =
bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
: mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
+ final StageTaskListener toTopStage = mainStageToTop ? mMainStage : mSideStage;
if (!ENABLE_SHELL_TRANSITIONS) {
- exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, reason);
+ exitSplitScreen(toTopStage, reason);
return;
}
final int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
final WindowContainerTransaction wct = new WindowContainerTransaction();
+ toTopStage.resetBounds(wct);
prepareExitSplitScreen(dismissTop, wct);
if (mRootTaskInfo != null) {
wct.setDoNotPip(mRootTaskInfo.token);
@@ -2531,8 +2535,17 @@
shouldAnimate = startPendingEnterAnimation(
mSplitTransitions.mPendingEnter, info, startTransaction, finishTransaction);
} else if (mSplitTransitions.isPendingDismiss(transition)) {
+ final SplitScreenTransitions.DismissSession dismiss = mSplitTransitions.mPendingDismiss;
shouldAnimate = startPendingDismissAnimation(
- mSplitTransitions.mPendingDismiss, info, startTransaction, finishTransaction);
+ dismiss, info, startTransaction, finishTransaction);
+ if (shouldAnimate && dismiss.mReason == EXIT_REASON_DRAG_DIVIDER) {
+ final StageTaskListener toTopStage =
+ dismiss.mDismissTop == STAGE_TYPE_MAIN ? mMainStage : mSideStage;
+ mSplitTransitions.playDragDismissAnimation(transition, info, startTransaction,
+ finishTransaction, finishCallback, toTopStage.mRootTaskInfo.token,
+ toTopStage.getSplitDecorManager(), mRootTaskInfo.token);
+ return true;
+ }
} else if (mSplitTransitions.isPendingResize(transition)) {
mSplitTransitions.playResizeAnimation(transition, info, startTransaction,
finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token,
@@ -2787,6 +2800,10 @@
mSplitTransitions.mPendingDismiss = null;
return false;
}
+ dismissTransition.setFinishedCallback((callbackWct, callbackT) -> {
+ mMainStage.getSplitDecorManager().release(callbackT);
+ mSideStage.getSplitDecorManager().release(callbackT);
+ });
addDividerBarToTransition(info, false /* show */);
return true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index e2e9270..da7d186 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -19,6 +19,7 @@
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
@@ -201,7 +202,7 @@
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
if (mRootTaskInfo.taskId == taskInfo.taskId) {
// Inflates split decor view only when the root task is visible.
- if (mRootTaskInfo.isVisible != taskInfo.isVisible) {
+ if (!ENABLE_SHELL_TRANSITIONS && mRootTaskInfo.isVisible != taskInfo.isVisible) {
if (taskInfo.isVisible) {
mSplitDecorManager.inflate(mContext, mRootLeash,
taskInfo.configuration.windowConfiguration.getBounds());
@@ -385,6 +386,7 @@
void resetBounds(WindowContainerTransaction wct) {
wct.setBounds(mRootTaskInfo.token, null);
wct.setAppBounds(mRootTaskInfo.token, null);
+ wct.setSmallestScreenWidthDp(mRootTaskInfo.token, SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
}
void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener,
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..3b306e7 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
@@ -191,6 +191,12 @@
*/
private static final int SYNC_ALLOWANCE_MS = 120;
+ /**
+ * Keyguard gets a more generous timeout to finish its animations, because we are always holding
+ * a sleep token during occlude/unocclude transitions and we want them to finish playing cleanly
+ */
+ private static final int SYNC_ALLOWANCE_KEYGUARD_MS = 2000;
+
/** For testing only. Disables the force-finish timeout on sync. */
private boolean mDisableForceSync = false;
@@ -492,6 +498,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);
}
}
}
@@ -669,7 +679,7 @@
// Sleep starts a process of forcing all prior transitions to finish immediately
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
"Start finish-for-sync track %d", i);
- finishForSync(i, null /* forceFinish */);
+ finishForSync(active, i, null /* forceFinish */);
}
if (hadPreceding) {
return false;
@@ -1017,6 +1027,9 @@
for (int i = 0; i < mPendingTransitions.size(); ++i) {
if (mPendingTransitions.get(i).mToken == token) return true;
}
+ for (int i = 0; i < mReadyDuringSync.size(); ++i) {
+ if (mReadyDuringSync.get(i).mToken == token) return true;
+ }
for (int t = 0; t < mTracks.size(); ++t) {
final Track tr = mTracks.get(t);
for (int i = 0; i < tr.mReadyTransitions.size(); ++i) {
@@ -1103,10 +1116,17 @@
*
* This is then repeated until there are no more pending sleep transitions.
*
+ * @param reason The SLEEP transition that triggered this round of finishes. We will continue
+ * looping round finishing transitions as long as this is still waiting.
* @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge
* signal to -- so it will be force-finished if it's still running.
*/
- private void finishForSync(int trackIdx, @Nullable ActiveTransition forceFinish) {
+ private void finishForSync(ActiveTransition reason,
+ int trackIdx, @Nullable ActiveTransition forceFinish) {
+ if (!isTransitionKnown(reason.mToken)) {
+ Log.d(TAG, "finishForSleep: already played sync transition " + reason);
+ return;
+ }
final Track track = mTracks.get(trackIdx);
if (forceFinish != null) {
final Track trk = mTracks.get(forceFinish.getTrack());
@@ -1150,8 +1170,11 @@
if (track.mActiveTransition == playing) {
if (!mDisableForceSync) {
// Give it a short amount of time to process it before forcing.
- mMainExecutor.executeDelayed(() -> finishForSync(trackIdx, playing),
- SYNC_ALLOWANCE_MS);
+ final int tolerance = KeyguardTransitionHandler.handles(playing.mInfo)
+ ? SYNC_ALLOWANCE_KEYGUARD_MS
+ : SYNC_ALLOWANCE_MS;
+ mMainExecutor.executeDelayed(
+ () -> finishForSync(reason, trackIdx, playing), tolerance);
}
break;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 9189d3d..fb17d87 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -223,7 +223,7 @@
doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask();
// Put the same component into a task in the background
ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo();
- doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any());
+ doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any(), anyInt());
mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
SPLIT_POSITION_TOP_OR_LEFT, null);
@@ -247,7 +247,7 @@
SPLIT_POSITION_BOTTOM_OR_RIGHT);
// Put the same component into a task in the background
doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks)
- .findTaskInBackground(any());
+ .findTaskInBackground(any(), anyInt());
mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null,
SPLIT_POSITION_TOP_OR_LEFT, null);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index ae69b3d..4e446c6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -44,11 +44,15 @@
static SplitLayout createMockSplitLayout() {
final Rect dividerBounds = new Rect(48, 0, 52, 100);
+ final Rect bounds1 = new Rect(0, 0, 40, 100);
+ final Rect bounds2 = new Rect(60, 0, 100, 100);
final SurfaceControl leash = createMockSurface();
SplitLayout out = mock(SplitLayout.class);
doReturn(dividerBounds).when(out).getDividerBounds();
doReturn(dividerBounds).when(out).getRefDividerBounds();
doReturn(leash).when(out).getDividerLeash();
+ doReturn(bounds1).when(out).getBounds1();
+ doReturn(bounds2).when(out).getBounds2();
return out;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 8038453..60c0e55 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -42,6 +42,7 @@
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -72,6 +73,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitDecorManager;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.transition.Transitions;
@@ -117,13 +119,13 @@
doReturn(mockExecutor).when(mTransitions).getAnimExecutor();
doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire();
mSplitLayout = SplitTestUtils.createMockSplitLayout();
- mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
+ mMainStage = spy(new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
- mIconProvider);
+ mIconProvider));
mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
- mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
+ mSideStage = spy(new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
- mIconProvider);
+ mIconProvider));
mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController,
@@ -137,6 +139,8 @@
.setParentTaskId(mMainStage.mRootTaskInfo.taskId).build();
mSideChild = new TestRunningTaskInfoBuilder()
.setParentTaskId(mSideStage.mRootTaskInfo.taskId).build();
+ doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager();
+ doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager();
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 6621ab8..66b6c62 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -68,6 +68,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitDecorManager;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
import com.android.wm.shell.sysui.ShellController;
@@ -145,6 +146,8 @@
mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
+ doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager();
+ doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager();
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index 8115a5d..ee9f886 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -364,6 +364,7 @@
1, HardwareBuffer.USAGE_CPU_READ_RARELY);
return new TaskSnapshot(
System.currentTimeMillis(),
+ 0 /* captureTime */,
new ComponentName("", ""), buffer,
ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */,
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/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index c4f09ce..f911d35 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -111,6 +111,9 @@
public static final int COMPLICATION_TYPE_SMARTSPACE = 7;
public static final int COMPLICATION_TYPE_MEDIA_ENTRY = 8;
+ private static final int SCREENSAVER_HOME_CONTROLS_ENABLED_DEFAULT = 1;
+ private static final int LOCKSCREEN_SHOW_CONTROLS_DEFAULT = 0;
+
private final Context mContext;
private final IDreamManager mDreamManager;
private final DreamInfoComparator mComparator;
@@ -311,8 +314,14 @@
/** Gets whether home controls button is enabled on the dream */
private boolean getHomeControlsEnabled() {
- return Settings.Secure.getInt(mContext.getContentResolver(),
- Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, 1) == 1;
+ return Settings.Secure.getInt(
+ mContext.getContentResolver(),
+ Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+ LOCKSCREEN_SHOW_CONTROLS_DEFAULT) == 1
+ && Settings.Secure.getInt(
+ mContext.getContentResolver(),
+ Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED,
+ SCREENSAVER_HOME_CONTROLS_ENABLED_DEFAULT) == 1;
}
/**
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
index 22ec12d..2edf403 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
@@ -28,6 +28,7 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
+import android.provider.Settings;
import org.junit.After;
import org.junit.Before;
@@ -84,6 +85,7 @@
@Test
public void testComplicationsEnabledByDefault() {
+ setControlsEnabledOnLockscreen(true);
assertThat(mBackend.getComplicationsEnabled()).isTrue();
assertThat(mBackend.getEnabledComplications()).containsExactlyElementsIn(
SUPPORTED_DREAM_COMPLICATIONS_LIST);
@@ -91,6 +93,7 @@
@Test
public void testEnableComplicationExplicitly() {
+ setControlsEnabledOnLockscreen(true);
mBackend.setComplicationsEnabled(true);
assertThat(mBackend.getEnabledComplications()).containsExactlyElementsIn(
SUPPORTED_DREAM_COMPLICATIONS_LIST);
@@ -99,6 +102,7 @@
@Test
public void testDisableComplications() {
+ setControlsEnabledOnLockscreen(true);
mBackend.setComplicationsEnabled(false);
assertThat(mBackend.getEnabledComplications())
.containsExactly(COMPLICATION_TYPE_HOME_CONTROLS);
@@ -107,6 +111,7 @@
@Test
public void testHomeControlsDisabled_ComplicationsEnabled() {
+ setControlsEnabledOnLockscreen(true);
mBackend.setComplicationsEnabled(true);
mBackend.setHomeControlsEnabled(false);
// Home controls should not be enabled, only date and time.
@@ -118,6 +123,7 @@
@Test
public void testHomeControlsDisabled_ComplicationsDisabled() {
+ setControlsEnabledOnLockscreen(true);
mBackend.setComplicationsEnabled(false);
mBackend.setHomeControlsEnabled(false);
assertThat(mBackend.getEnabledComplications()).isEmpty();
@@ -125,9 +131,9 @@
@Test
public void testHomeControlsEnabled_ComplicationsDisabled() {
+ setControlsEnabledOnLockscreen(true);
mBackend.setComplicationsEnabled(false);
mBackend.setHomeControlsEnabled(true);
- // Home controls should not be enabled, only date and time.
final List<Integer> enabledComplications =
Collections.singletonList(COMPLICATION_TYPE_HOME_CONTROLS);
assertThat(mBackend.getEnabledComplications())
@@ -136,9 +142,9 @@
@Test
public void testHomeControlsEnabled_ComplicationsEnabled() {
+ setControlsEnabledOnLockscreen(true);
mBackend.setComplicationsEnabled(true);
mBackend.setHomeControlsEnabled(true);
- // Home controls should not be enabled, only date and time.
final List<Integer> enabledComplications =
Arrays.asList(
COMPLICATION_TYPE_HOME_CONTROLS,
@@ -148,4 +154,26 @@
assertThat(mBackend.getEnabledComplications())
.containsExactlyElementsIn(enabledComplications);
}
+
+ @Test
+ public void testHomeControlsEnabled_lockscreenDisabled() {
+ setControlsEnabledOnLockscreen(false);
+ mBackend.setComplicationsEnabled(true);
+ mBackend.setHomeControlsEnabled(true);
+ // Home controls should not be enabled, only date and time.
+ final List<Integer> enabledComplications =
+ Arrays.asList(
+ COMPLICATION_TYPE_DATE,
+ COMPLICATION_TYPE_TIME
+ );
+ assertThat(mBackend.getEnabledComplications())
+ .containsExactlyElementsIn(enabledComplications);
+ }
+
+ private void setControlsEnabledOnLockscreen(boolean enabled) {
+ Settings.Secure.putInt(
+ mContext.getContentResolver(),
+ Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+ enabled ? 1 : 0);
+ }
}
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index bdd941d..aee829d 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -90,63 +90,6 @@
//
// If you don't use @Postsubmit, your new test will immediately
// block presubmit, which is probably not what you want!
- "sysui-platinum-postsubmit": [
- {
- "name": "PlatformScenarioTests",
- "options": [
- {
- "include-filter": "android.platform.test.scenario.sysui"
- },
- {
- "include-annotation": "android.platform.test.scenario.annotation.Scenario"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- },
- {
- "exclude-annotation": "android.platform.test.annotations.FlakyTest"
- }
- ]
- }
- ],
- "sysui-staged-platinum-postsubmit": [
- {
- "name": "PlatformScenarioTests",
- "options": [
- {
- "include-filter": "android.platform.test.scenario.sysui"
- },
- {
- "include-annotation": "android.platform.test.scenario.annotation.Scenario"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- }
- ]
- }
- ],
- "ironwood-postsubmit": [
- {
- "name": "PlatformScenarioTests",
- "options": [
- {
- "include-annotation": "android.platform.test.annotations.IwTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- },
- {
- "include-filter": "android.platform.test.scenario.sysui"
- },
- {
- "exclude-annotation": "android.platform.test.annotations.FlakyTest"
- }
- ]
- }
- ],
"auto-end-to-end-postsubmit": [
{
"name": "AndroidAutomotiveHomeTests",
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 8dd2c39..465b73e 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -272,6 +272,7 @@
color = lockScreenColor,
animate = isAnimationEnabled,
duration = APPEAR_ANIM_DURATION,
+ interpolator = Interpolators.EMPHASIZED_DECELERATE,
delay = 0,
onAnimationEnd = null
)
@@ -562,7 +563,7 @@
private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"
private const val DOZE_ANIM_DURATION: Long = 300
- private const val APPEAR_ANIM_DURATION: Long = 350
+ private const val APPEAR_ANIM_DURATION: Long = 833
private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
private const val COLOR_ANIM_DURATION: Long = 400
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index cad2c16..4b79689 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -96,6 +96,7 @@
<!-- additional offset for clock switch area items -->
<dimen name="small_clock_height">114dp</dimen>
+ <dimen name="small_clock_padding_top">28dp</dimen>
<dimen name="clock_padding_start">28dp</dimen>
<dimen name="below_clock_padding_start">32dp</dimen>
<dimen name="below_clock_padding_end">16dp</dimen>
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/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a62dead..8d3ba36 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -716,7 +716,7 @@
<!-- Minimum margin between clock and status bar -->
<dimen name="keyguard_clock_top_margin">18dp</dimen>
<!-- The amount to shift the clocks during a small/large transition -->
- <dimen name="keyguard_clock_switch_y_shift">10dp</dimen>
+ <dimen name="keyguard_clock_switch_y_shift">14dp</dimen>
<!-- When large clock is showing, offset the smartspace by this amount -->
<dimen name="keyguard_smartspace_top_offset">12dp</dimen>
<!-- With the large clock, move up slightly from the center -->
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/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index e54d473..d9d64ad 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -1,5 +1,8 @@
package com.android.keyguard;
+import static android.view.View.ALPHA;
+import static android.view.View.TRANSLATION_Y;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -35,11 +38,12 @@
private static final String TAG = "KeyguardClockSwitch";
- private static final long CLOCK_OUT_MILLIS = 150;
- private static final long CLOCK_IN_MILLIS = 200;
- public static final long CLOCK_IN_START_DELAY_MILLIS = CLOCK_OUT_MILLIS / 2;
- private static final long STATUS_AREA_START_DELAY_MILLIS = 50;
- private static final long STATUS_AREA_MOVE_MILLIS = 350;
+ private static final long CLOCK_OUT_MILLIS = 133;
+ private static final long CLOCK_IN_MILLIS = 167;
+ public static final long CLOCK_IN_START_DELAY_MILLIS = 133;
+ private static final long STATUS_AREA_START_DELAY_MILLIS = 0;
+ private static final long STATUS_AREA_MOVE_UP_MILLIS = 967;
+ private static final long STATUS_AREA_MOVE_DOWN_MILLIS = 467;
@IntDef({LARGE, SMALL})
@Retention(RetentionPolicy.SOURCE)
@@ -66,6 +70,17 @@
top + targetHeight);
}
+ /** Returns a region for the small clock to position itself, based on the given parent. */
+ public static Rect getSmallClockRegion(ViewGroup parent) {
+ int targetHeight = parent.getResources()
+ .getDimensionPixelSize(R.dimen.small_clock_text_size);
+ return new Rect(
+ parent.getLeft(),
+ parent.getTop(),
+ parent.getRight(),
+ parent.getTop() + targetHeight);
+ }
+
/**
* Frame for small/large clocks
*/
@@ -90,7 +105,7 @@
@VisibleForTesting AnimatorSet mClockInAnim = null;
@VisibleForTesting AnimatorSet mClockOutAnim = null;
- private ObjectAnimator mStatusAreaAnim = null;
+ private AnimatorSet mStatusAreaAnim = null;
private int mClockSwitchYAmount;
@VisibleForTesting boolean mChildrenAreLaidOut = false;
@@ -172,13 +187,8 @@
void updateClockTargetRegions() {
if (mClock != null) {
if (mSmallClockFrame.isLaidOut()) {
- int targetHeight = getResources()
- .getDimensionPixelSize(R.dimen.small_clock_text_size);
- mClock.getSmallClock().getEvents().onTargetRegionChanged(new Rect(
- mSmallClockFrame.getLeft(),
- mSmallClockFrame.getTop(),
- mSmallClockFrame.getRight(),
- mSmallClockFrame.getTop() + targetHeight));
+ Rect targetRegion = getSmallClockRegion(mSmallClockFrame);
+ mClock.getSmallClock().getEvents().onTargetRegionChanged(targetRegion);
}
if (mLargeClockFrame.isLaidOut()) {
@@ -220,39 +230,44 @@
mStatusAreaAnim = null;
View in, out;
- int direction = 1;
- float statusAreaYTranslation;
+ float statusAreaYTranslation, clockInYTranslation, clockOutYTranslation;
if (useLargeClock) {
out = mSmallClockFrame;
in = mLargeClockFrame;
if (indexOfChild(in) == -1) addView(in, 0);
- direction = -1;
statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop()
+ mSmartspaceTopOffset;
+ clockInYTranslation = 0;
+ clockOutYTranslation = 0; // Small clock translation is handled with statusArea
} else {
in = mSmallClockFrame;
out = mLargeClockFrame;
statusAreaYTranslation = 0f;
+ clockInYTranslation = 0f;
+ clockOutYTranslation = mClockSwitchYAmount * -1f;
- // Must remove in order for notifications to appear in the proper place
+ // Must remove in order for notifications to appear in the proper place, ideally this
+ // would happen after the out animation runs, but we can't guarantee that the
+ // nofications won't enter only after the out animation runs.
removeView(out);
}
if (!animate) {
out.setAlpha(0f);
+ out.setTranslationY(clockOutYTranslation);
in.setAlpha(1f);
- in.setVisibility(VISIBLE);
+ in.setTranslationY(clockInYTranslation);
+ in.setVisibility(View.VISIBLE);
mStatusArea.setTranslationY(statusAreaYTranslation);
return;
}
mClockOutAnim = new AnimatorSet();
mClockOutAnim.setDuration(CLOCK_OUT_MILLIS);
- mClockOutAnim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+ mClockOutAnim.setInterpolator(Interpolators.LINEAR);
mClockOutAnim.playTogether(
- ObjectAnimator.ofFloat(out, View.ALPHA, 0f),
- ObjectAnimator.ofFloat(out, View.TRANSLATION_Y, 0,
- direction * -mClockSwitchYAmount));
+ ObjectAnimator.ofFloat(out, ALPHA, 0f),
+ ObjectAnimator.ofFloat(out, TRANSLATION_Y, clockOutYTranslation));
mClockOutAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
mClockOutAnim = null;
@@ -264,8 +279,9 @@
mClockInAnim = new AnimatorSet();
mClockInAnim.setDuration(CLOCK_IN_MILLIS);
mClockInAnim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
- mClockInAnim.playTogether(ObjectAnimator.ofFloat(in, View.ALPHA, 1f),
- ObjectAnimator.ofFloat(in, View.TRANSLATION_Y, direction * mClockSwitchYAmount, 0));
+ mClockInAnim.playTogether(
+ ObjectAnimator.ofFloat(in, ALPHA, 1f),
+ ObjectAnimator.ofFloat(in, TRANSLATION_Y, clockInYTranslation));
mClockInAnim.setStartDelay(CLOCK_IN_START_DELAY_MILLIS);
mClockInAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
@@ -273,19 +289,22 @@
}
});
- mClockInAnim.start();
- mClockOutAnim.start();
-
- mStatusAreaAnim = ObjectAnimator.ofFloat(mStatusArea, View.TRANSLATION_Y,
- statusAreaYTranslation);
- mStatusAreaAnim.setStartDelay(useLargeClock ? STATUS_AREA_START_DELAY_MILLIS : 0L);
- mStatusAreaAnim.setDuration(STATUS_AREA_MOVE_MILLIS);
- mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mStatusAreaAnim = new AnimatorSet();
+ mStatusAreaAnim.setStartDelay(STATUS_AREA_START_DELAY_MILLIS);
+ mStatusAreaAnim.setDuration(
+ useLargeClock ? STATUS_AREA_MOVE_UP_MILLIS : STATUS_AREA_MOVE_DOWN_MILLIS);
+ mStatusAreaAnim.setInterpolator(Interpolators.EMPHASIZED);
+ mStatusAreaAnim.playTogether(
+ ObjectAnimator.ofFloat(mStatusArea, TRANSLATION_Y, statusAreaYTranslation),
+ ObjectAnimator.ofFloat(mSmallClockFrame, TRANSLATION_Y, statusAreaYTranslation));
mStatusAreaAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
mStatusAreaAnim = null;
}
});
+
+ mClockInAnim.start();
+ mClockOutAnim.start();
mStatusAreaAnim.start();
}
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/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
index f04fdfff..9807b9e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt
@@ -122,7 +122,9 @@
if (shouldAnimateIconViewForTransition(lastState, newState)) {
iconView.playAnimation()
}
- LottieColorUtils.applyDynamicColors(context, iconView)
+ if (isSideFps) {
+ LottieColorUtils.applyDynamicColors(context, iconView)
+ }
}
override fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int) {
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/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
index ede62ac..a3f34ce 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
@@ -68,15 +68,15 @@
var inputTopBound: Int
var headerRightBound = right
var headerTopBounds = top
+ var headerBottomBounds = bottom
val subTitleBottom: Int = if (subtitleView.isGone) titleView.bottom else subtitleView.bottom
val descBottom = if (descriptionView.isGone) subTitleBottom else descriptionView.bottom
if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) {
inputTopBound = (bottom - credentialInput.height) / 2
inputLeftBound = (right - left) / 2
headerRightBound = inputLeftBound
- headerTopBounds -= iconView.bottom.coerceAtMost(bottomInset)
-
- if (descriptionView.bottom > bottomInset) {
+ if (descriptionView.bottom > headerBottomBounds) {
+ headerTopBounds -= iconView.bottom.coerceAtMost(bottomInset)
credentialHeader.layout(left, headerTopBounds, headerRightBound, bottom)
}
} else {
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/complication/ComplicationTypesUpdater.java b/packages/SystemUI/src/com/android/systemui/complication/ComplicationTypesUpdater.java
index a334c1e..0bdc7f1 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/ComplicationTypesUpdater.java
+++ b/packages/SystemUI/src/com/android/systemui/complication/ComplicationTypesUpdater.java
@@ -77,6 +77,10 @@
Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED,
settingsObserver,
UserHandle.myUserId());
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.LOCKSCREEN_SHOW_CONTROLS,
+ settingsObserver,
+ UserHandle.myUserId());
settingsObserver.onChange(false);
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e118fdf..19810b3 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
@@ -167,7 +177,7 @@
*/
// TODO(b/281655028): Tracking bug
@JvmField
- val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = true)
+ val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = false)
/** Flag to control the migration of face auth to modern architecture. */
// TODO(b/262838215): Tracking bug
@@ -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.
@@ -525,13 +535,6 @@
val ENABLE_PIP_APP_ICON_OVERLAY =
sysPropBooleanFlag(1115, "persist.wm.debug.enable_pip_app_icon_overlay", default = true)
- // TODO(b/272110828): Tracking bug
- @Keep
- @JvmField
- val ENABLE_MOVE_FLOATING_WINDOW_IN_TABLETOP =
- sysPropBooleanFlag(
- 1116, "persist.wm.debug.enable_move_floating_window_in_tabletop", default = true)
-
// TODO(b/273443374): Tracking Bug
@Keep
@JvmField val LOCKSCREEN_LIVE_WALLPAPER =
@@ -615,8 +618,6 @@
unreleasedFlag(1401, "quick_tap_flow_framework", teamfood = false)
// 1500 - chooser aka sharesheet
- // TODO(b/254512507): Tracking Bug
- val CHOOSER_UNBUNDLED = releasedFlag(1500, "chooser_unbundled")
// 1700 - clipboard
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 54da680..b8d3121 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -19,7 +19,6 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
-import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
@@ -30,13 +29,11 @@
import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_OCCLUDE_BY_DREAM;
import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE;
import static android.view.WindowManager.TRANSIT_OLD_NONE;
-import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.TransitionFlags;
import static android.view.WindowManager.TransitionOldType;
import static android.view.WindowManager.TransitionType;
+import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.Service;
@@ -65,6 +62,7 @@
import android.window.IRemoteTransitionFinishedCallback;
import android.window.TransitionInfo;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.internal.policy.IKeyguardDrawnCallback;
import com.android.internal.policy.IKeyguardExitCallback;
@@ -116,6 +114,14 @@
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
final int taskId = taskInfo != null ? change.getTaskInfo().taskId : -1;
+ if (taskId != -1 && change.getParent() != null) {
+ final TransitionInfo.Change parentChange = info.getChange(change.getParent());
+ if (parentChange != null && parentChange.getTaskInfo() != null) {
+ // Only adding the root task as the animation target.
+ continue;
+ }
+ }
+
final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
// wallpapers go into the "below" layer space
info.getChanges().size() - i,
@@ -123,13 +129,6 @@
(change.getFlags() & TransitionInfo.FLAG_SHOW_WALLPAPER) != 0,
info, t, leashMap);
- // Use hasAnimatingParent to mark the anything below root task
- if (taskId != -1 && change.getParent() != null) {
- final TransitionInfo.Change parentChange = info.getChange(change.getParent());
- if (parentChange != null && parentChange.getTaskInfo() != null) {
- target.hasAnimatingParent = true;
- }
- }
out.add(target);
}
return out.toArray(new RemoteAnimationTarget[out.size()]);
@@ -158,71 +157,83 @@
// Note: Also used for wrapping occlude by Dream animation. It works (with some redundancy).
public static IRemoteTransition wrap(IRemoteAnimationRunner runner) {
return new IRemoteTransition.Stub() {
- final ArrayMap<IBinder, IRemoteTransitionFinishedCallback> mFinishCallbacks =
- new ArrayMap<>();
+
private final ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = new ArrayMap<>();
+ @GuardedBy("mLeashMap")
+ private IRemoteTransitionFinishedCallback mFinishCallback = null;
+
@Override
public void startAnimation(IBinder transition, TransitionInfo info,
SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)
throws RemoteException {
Slog.d(TAG, "Starts IRemoteAnimationRunner: info=" + info);
- final RemoteAnimationTarget[] apps =
- wrap(info, false /* wallpapers */, t, mLeashMap);
- final RemoteAnimationTarget[] wallpapers =
- wrap(info, true /* wallpapers */, t, mLeashMap);
- final RemoteAnimationTarget[] nonApps = new RemoteAnimationTarget[0];
- // Sets the alpha to 0 for the opening root task for fade in animation. And since
- // the fade in animation can only apply on the first opening app, so set alpha to 1
- // for anything else.
- for (RemoteAnimationTarget target : apps) {
- if (target.taskId != -1
- && target.mode == RemoteAnimationTarget.MODE_OPENING
- && !target.hasAnimatingParent) {
- t.setAlpha(target.leash, 0.0f);
- } else {
- t.setAlpha(target.leash, 1.0f);
- }
- }
- t.apply();
- synchronized (mFinishCallbacks) {
- mFinishCallbacks.put(transition, finishCallback);
- }
- runner.onAnimationStart(getTransitionOldType(info.getType(), info.getFlags(), apps),
- apps, wallpapers, nonApps,
- new IRemoteAnimationFinishedCallback.Stub() {
- @Override
- public void onAnimationFinished() throws RemoteException {
- synchronized (mFinishCallbacks) {
- if (mFinishCallbacks.remove(transition) == null) return;
- }
- info.releaseAllSurfaces();
- Slog.d(TAG, "Finish IRemoteAnimationRunner.");
- finishCallback.onTransitionFinished(null /* wct */, null /* t */);
- }
+ synchronized (mLeashMap) {
+ final RemoteAnimationTarget[] apps =
+ wrap(info, false /* wallpapers */, t, mLeashMap);
+ final RemoteAnimationTarget[] wallpapers =
+ wrap(info, true /* wallpapers */, t, mLeashMap);
+ final RemoteAnimationTarget[] nonApps = new RemoteAnimationTarget[0];
+
+ // Set alpha back to 1 for the independent changes because we will be animating
+ // children instead.
+ for (TransitionInfo.Change chg : info.getChanges()) {
+ if (TransitionInfo.isIndependent(chg, info)) {
+ t.setAlpha(chg.getLeash(), 1.f);
}
- );
+ }
+ initAlphaForAnimationTargets(t, apps);
+ initAlphaForAnimationTargets(t, wallpapers);
+ t.apply();
+ mFinishCallback = finishCallback;
+ runner.onAnimationStart(
+ getTransitionOldType(info.getType(), info.getFlags(), apps),
+ apps, wallpapers, nonApps,
+ new IRemoteAnimationFinishedCallback.Stub() {
+ @Override
+ public void onAnimationFinished() throws RemoteException {
+ synchronized (mLeashMap) {
+ Slog.d(TAG, "Finish IRemoteAnimationRunner.");
+ finish();
+ }
+ }
+ }
+ );
+ }
}
public void mergeAnimation(IBinder candidateTransition, TransitionInfo candidateInfo,
SurfaceControl.Transaction candidateT, IBinder currentTransition,
- IRemoteTransitionFinishedCallback candidateFinishCallback) {
+ IRemoteTransitionFinishedCallback candidateFinishCallback)
+ throws RemoteException {
try {
- final IRemoteTransitionFinishedCallback currentFinishCB;
- synchronized (mFinishCallbacks) {
- currentFinishCB = mFinishCallbacks.remove(currentTransition);
+ synchronized (mLeashMap) {
+ runner.onAnimationCancelled();
+ finish();
}
- if (currentFinishCB == null) {
- Slog.e(TAG, "Called mergeAnimation, but finish callback is missing");
- return;
- }
- runner.onAnimationCancelled();
- currentFinishCB.onTransitionFinished(null /* wct */, null /* t */);
} catch (RemoteException e) {
// nothing, we'll just let it finish on its own I guess.
}
}
+
+ private static void initAlphaForAnimationTargets(@NonNull SurfaceControl.Transaction t,
+ @NonNull RemoteAnimationTarget[] targets) {
+ for (RemoteAnimationTarget target : targets) {
+ if (target.mode != MODE_OPENING) continue;
+ t.setAlpha(target.leash, 0.f);
+ }
+ }
+
+ @GuardedBy("mLeashMap")
+ private void finish() throws RemoteException {
+ mLeashMap.clear();
+ final IRemoteTransitionFinishedCallback finishCallback = mFinishCallback;
+ if (finishCallback != null) {
+ mFinishCallback = null;
+ finishCallback.onTransitionFinished(null /* wct */, null /* t */);
+ }
+ }
};
}
@@ -333,15 +344,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 +375,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 +386,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 +418,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 +431,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 +443,7 @@
@Override // Binder interface
public void onFinishedWakingUp() {
+ trace("onFinishedWakingUp");
Trace.beginSection("KeyguardService.mBinder#onFinishedWakingUp");
checkPermission();
mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.FINISHED_WAKING_UP);
@@ -417,6 +452,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 +487,7 @@
@Override // Binder interface
public void onScreenTurnedOn() {
+ trace("onScreenTurnedOn");
Trace.beginSection("KeyguardService.mBinder#onScreenTurnedOn");
checkPermission();
mKeyguardLifecyclesDispatcher.dispatch(KeyguardLifecyclesDispatcher.SCREEN_TURNED_ON);
@@ -460,12 +497,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 +513,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 +529,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 +560,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/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index f96f337..122e259 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -812,8 +812,8 @@
// Translate up from the bottom.
surfaceBehindMatrix.setTranslate(
- surfaceBehindRemoteAnimationTarget.localBounds.left.toFloat(),
- surfaceBehindRemoteAnimationTarget.localBounds.top.toFloat() +
+ surfaceBehindRemoteAnimationTarget.screenSpaceBounds.left.toFloat(),
+ surfaceBehindRemoteAnimationTarget.screenSpaceBounds.top.toFloat() +
surfaceHeight * SURFACE_BEHIND_START_TRANSLATION_Y * (1f - amount)
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
index d8affa4..1978b3d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
@@ -139,7 +139,8 @@
if (dozeDisabledAndScreenOff || dozeEnabledAndDozeAnimationCompleted) {
Trace.beginSection("ResourceTrimmer#trimMemory")
Log.d(LOG_TAG, "SysUI asleep, trimming memory.")
- globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
+ globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+ globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
Trace.endSection()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
new file mode 100644
index 0000000..641e20b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.keyguard.data.repository
+
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.shared.model.SettingsClockSize
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class KeyguardClockRepository
+@Inject
+constructor(
+ private val secureSettings: SecureSettings,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+
+ val selectedClockSize: Flow<SettingsClockSize> =
+ secureSettings
+ .observerFlow(
+ names = arrayOf(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK),
+ userId = UserHandle.USER_SYSTEM,
+ )
+ .onStart { emit(Unit) } // Forces an initial update.
+ .map { getClockSize() }
+
+ private suspend fun getClockSize(): SettingsClockSize {
+ return withContext(backgroundDispatcher) {
+ if (
+ secureSettings.getIntForUser(
+ Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
+ 1,
+ UserHandle.USER_CURRENT
+ ) == 1
+ ) {
+ SettingsClockSize.DYNAMIC
+ } else {
+ SettingsClockSize.SMALL
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
new file mode 100644
index 0000000..98f445c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardClockRepository
+import com.android.systemui.keyguard.shared.model.SettingsClockSize
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Encapsulates business-logic related to the keyguard clock. */
+@SysUISingleton
+class KeyguardClockInteractor
+@Inject
+constructor(
+ repository: KeyguardClockRepository,
+) {
+ val selectedClockSize: Flow<SettingsClockSize> = repository.selectedClockSize
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/SettingsClockSize.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/SettingsClockSize.kt
new file mode 100644
index 0000000..c6b0f58
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/SettingsClockSize.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.keyguard.shared.model
+
+enum class SettingsClockSize {
+ DYNAMIC,
+ SMALL,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockSmartspaceViewBinder.kt
new file mode 100644
index 0000000..57c32b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockSmartspaceViewBinder.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.keyguard.ui.binder
+
+import android.view.View
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockSmartspaceViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import kotlinx.coroutines.flow.collect
+
+/** Binder for the small clock view, large clock view and smartspace. */
+object KeyguardPreviewClockSmartspaceViewBinder {
+
+ @JvmStatic
+ fun bind(
+ largeClockHostView: View,
+ smallClockHostView: View,
+ smartspace: View?,
+ viewModel: KeyguardPreviewClockSmartspaceViewModel,
+ ) {
+ largeClockHostView.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.isLargeClockVisible.collect { largeClockHostView.isVisible = it }
+ }
+ }
+
+ smallClockHostView.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.isSmallClockVisible.collect { smallClockHostView.isVisible = it }
+ }
+ }
+
+ smartspace?.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.smartSpaceTopPadding.collect { smartspace.setTopPadding(it) }
+ }
+ }
+ }
+
+ private fun View.setTopPadding(padding: Int) {
+ setPaddingRelative(paddingStart, padding, paddingEnd, paddingBottom)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 555a09b..4308d84 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -22,6 +22,7 @@
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.content.res.Resources
import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.os.Bundle
@@ -33,6 +34,7 @@
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
+import androidx.core.view.isInvisible
import com.android.keyguard.ClockEventController
import com.android.keyguard.KeyguardClockSwitch
import com.android.systemui.R
@@ -40,7 +42,10 @@
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockSmartspaceViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockSmartspaceViewModel
+import com.android.systemui.plugins.ClockController
import com.android.systemui.shared.clocks.ClockRegistry
import com.android.systemui.shared.clocks.DefaultClockController
import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants
@@ -60,6 +65,7 @@
@Application private val context: Context,
@Main private val mainDispatcher: CoroutineDispatcher,
@Main private val mainHandler: Handler,
+ private val clockSmartspaceViewModel: KeyguardPreviewClockSmartspaceViewModel,
private val bottomAreaViewModel: KeyguardBottomAreaViewModel,
displayManager: DisplayManager,
private val windowManager: WindowManager,
@@ -79,6 +85,7 @@
KeyguardPreviewConstants.KEY_HIGHLIGHT_QUICK_AFFORDANCES,
false,
)
+ /** [shouldHideClock] here means that we never create and bind the clock views */
private val shouldHideClock: Boolean =
bundle.getBoolean(ClockPreviewConstants.KEY_HIDE_CLOCK, false)
@@ -87,7 +94,8 @@
val surfacePackage: SurfaceControlViewHost.SurfacePackage
get() = host.surfacePackage
- private var clockView: View? = null
+ private lateinit var largeClockHostView: FrameLayout
+ private lateinit var smallClockHostView: FrameLayout
private var smartSpaceView: View? = null
private var colorOverride: Int? = null
@@ -126,6 +134,12 @@
if (!shouldHideClock) {
setUpClock(rootView)
+ KeyguardPreviewClockSmartspaceViewBinder.bind(
+ largeClockHostView,
+ smallClockHostView,
+ smartSpaceView,
+ clockSmartspaceViewModel,
+ )
}
rootView.measure(
@@ -205,11 +219,9 @@
smartSpaceView = lockscreenSmartspaceController.buildAndConnectDateView(parentView)
val topPadding: Int =
- with(context.resources) {
- getDimensionPixelSize(R.dimen.status_bar_header_height_keyguard) +
- getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
- getDimensionPixelSize(R.dimen.keyguard_clock_top_margin)
- }
+ KeyguardPreviewClockSmartspaceViewModel.getLargeClockSmartspaceTopPadding(
+ context.resources
+ )
val startPadding: Int =
with(context.resources) {
@@ -284,10 +296,19 @@
}
private fun setUpClock(parentView: ViewGroup) {
+ largeClockHostView = createLargeClockHostView()
+ largeClockHostView.isInvisible = true
+ parentView.addView(largeClockHostView)
+
+ smallClockHostView = createSmallClockHostView(parentView.resources)
+ smallClockHostView.isInvisible = true
+ parentView.addView(smallClockHostView)
+
+ // TODO (b/283465254): Move the listeners to KeyguardClockRepository
val clockChangeListener =
object : ClockRegistry.ClockChangeListener {
override fun onCurrentClockChanged() {
- onClockChanged(parentView)
+ onClockChanged()
}
}
clockRegistry.registerClockChangeListener(clockChangeListener)
@@ -317,62 +338,89 @@
disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) })
val layoutChangeListener =
- object : View.OnLayoutChangeListener {
- override fun onLayoutChange(
- v: View,
- left: Int,
- top: Int,
- right: Int,
- bottom: Int,
- oldLeft: Int,
- oldTop: Int,
- oldRight: Int,
- oldBottom: Int
- ) {
- if (clockController.clock !is DefaultClockController) {
- clockController.clock
- ?.largeClock
- ?.events
- ?.onTargetRegionChanged(
- KeyguardClockSwitch.getLargeClockRegion(parentView)
- )
- }
+ View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+ if (clockController.clock !is DefaultClockController) {
+ clockController.clock
+ ?.largeClock
+ ?.events
+ ?.onTargetRegionChanged(KeyguardClockSwitch.getLargeClockRegion(parentView))
}
}
-
parentView.addOnLayoutChangeListener(layoutChangeListener)
-
disposables.add(
DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) }
)
- onClockChanged(parentView)
+ onClockChanged()
}
- private fun onClockChanged(parentView: ViewGroup) {
+ private fun createLargeClockHostView(): FrameLayout {
+ val hostView = FrameLayout(context)
+ hostView.layoutParams =
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ )
+ return hostView
+ }
+
+ private fun createSmallClockHostView(resources: Resources): FrameLayout {
+ val hostView = FrameLayout(context)
+ val layoutParams =
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ resources.getDimensionPixelSize(R.dimen.small_clock_height)
+ )
+ layoutParams.topMargin =
+ KeyguardPreviewClockSmartspaceViewModel.getStatusBarHeight(resources) +
+ resources.getDimensionPixelSize(R.dimen.small_clock_padding_top)
+ hostView.layoutParams = layoutParams
+
+ hostView.setPaddingRelative(
+ resources.getDimensionPixelSize(R.dimen.clock_padding_start),
+ 0,
+ 0,
+ 0
+ )
+ hostView.clipChildren = false
+ return hostView
+ }
+
+ private fun onClockChanged() {
val clock = clockRegistry.createCurrentClock()
clockController.clock = clock
colorOverride?.let { clock.events.onSeedColorChanged(it) }
- clock.largeClock.events.onTargetRegionChanged(
- KeyguardClockSwitch.getLargeClockRegion(parentView)
- )
-
- clockView?.let { parentView.removeView(it) }
- clockView =
- clock.largeClock.view.apply {
- if (shouldHighlightSelectedAffordance) {
- alpha = DIM_ALPHA
- }
- parentView.addView(this)
- visibility = View.VISIBLE
- }
+ updateLargeClock(clock)
+ updateSmallClock(clock)
// Hide smart space if the clock has weather display; otherwise show it
hideSmartspace(clock.largeClock.config.hasCustomWeatherDataDisplay)
}
+ private fun updateLargeClock(clock: ClockController) {
+ clock.largeClock.events.onTargetRegionChanged(
+ KeyguardClockSwitch.getLargeClockRegion(largeClockHostView)
+ )
+ if (shouldHighlightSelectedAffordance) {
+ clock.largeClock.view.alpha = DIM_ALPHA
+ }
+ largeClockHostView.removeAllViews()
+ largeClockHostView.addView(clock.largeClock.view)
+ }
+
+ private fun updateSmallClock(clock: ClockController) {
+ clock.smallClock.events.onTargetRegionChanged(
+ KeyguardClockSwitch.getSmallClockRegion(smallClockHostView)
+ )
+ if (shouldHighlightSelectedAffordance) {
+ clock.smallClock.view.alpha = DIM_ALPHA
+ }
+ smallClockHostView.removeAllViews()
+ smallClockHostView.addView(clock.smallClock.view)
+ }
+
companion object {
private const val KEY_HOST_TOKEN = "host_token"
private const val KEY_VIEW_WIDTH = "width"
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockSmartspaceViewModel.kt
new file mode 100644
index 0000000..00c603b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockSmartspaceViewModel.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import android.content.Context
+import android.content.res.Resources
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+import com.android.systemui.keyguard.shared.model.SettingsClockSize
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** View model for the small clock view, large clock view and smartspace. */
+class KeyguardPreviewClockSmartspaceViewModel
+@Inject
+constructor(
+ @Application private val context: Context,
+ interactor: KeyguardClockInteractor,
+) {
+
+ val isLargeClockVisible: Flow<Boolean> =
+ interactor.selectedClockSize.map { it == SettingsClockSize.DYNAMIC }
+
+ val isSmallClockVisible: Flow<Boolean> =
+ interactor.selectedClockSize.map { it == SettingsClockSize.SMALL }
+
+ val smartSpaceTopPadding: Flow<Int> =
+ interactor.selectedClockSize.map {
+ when (it) {
+ SettingsClockSize.DYNAMIC -> getLargeClockSmartspaceTopPadding(context.resources)
+ SettingsClockSize.SMALL -> getSmallClockSmartspaceTopPadding(context.resources)
+ }
+ }
+
+ companion object {
+ fun getLargeClockSmartspaceTopPadding(resources: Resources): Int {
+ return with(resources) {
+ getDimensionPixelSize(R.dimen.status_bar_header_height_keyguard) +
+ getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
+ getDimensionPixelSize(R.dimen.keyguard_clock_top_margin)
+ }
+ }
+
+ fun getSmallClockSmartspaceTopPadding(resources: Resources): Int {
+ return with(resources) {
+ getStatusBarHeight(this) +
+ getDimensionPixelSize(R.dimen.small_clock_padding_top) +
+ getDimensionPixelSize(R.dimen.small_clock_height)
+ }
+ }
+
+ fun getStatusBarHeight(resource: Resources): Int {
+ var result = 0
+ val resourceId: Int = resource.getIdentifier("status_bar_height", "dimen", "android")
+ if (resourceId > 0) {
+ result = resource.getDimensionPixelSize(resourceId)
+ }
+ return result
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index 1469d96..bce3346 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -16,10 +16,12 @@
package com.android.systemui.media.controls.pipeline
+import android.annotation.SuppressLint
import android.app.BroadcastOptions
import android.app.Notification
import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
import android.app.PendingIntent
+import android.app.StatusBarManager
import android.app.smartspace.SmartspaceConfig
import android.app.smartspace.SmartspaceManager
import android.app.smartspace.SmartspaceSession
@@ -43,7 +45,6 @@
import android.net.Uri
import android.os.Parcelable
import android.os.Process
-import android.os.RemoteException
import android.os.UserHandle
import android.provider.Settings
import android.service.notification.StatusBarNotification
@@ -53,7 +54,6 @@
import android.util.Pair as APair
import androidx.media.utils.MediaConstants
import com.android.internal.logging.InstanceId
-import com.android.internal.statusbar.IStatusBarService
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Dumpable
import com.android.systemui.R
@@ -185,7 +185,6 @@
private val logger: MediaUiEventLogger,
private val smartspaceManager: SmartspaceManager,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- private val statusBarService: IStatusBarService,
) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
companion object {
@@ -230,6 +229,10 @@
private val artworkHeight =
context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded)
+ @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE
+ private val statusBarManager =
+ context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager
+
/** Check whether this notification is an RCN */
private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
@@ -257,7 +260,6 @@
mediaFlags: MediaFlags,
logger: MediaUiEventLogger,
smartspaceManager: SmartspaceManager,
- statusBarService: IStatusBarService,
keyguardUpdateMonitor: KeyguardUpdateMonitor,
) : this(
context,
@@ -283,7 +285,6 @@
logger,
smartspaceManager,
keyguardUpdateMonitor,
- statusBarService,
)
private val appChangeReceiver =
@@ -793,27 +794,12 @@
song = HybridGroupManager.resolveTitle(notif)
}
if (song.isNullOrBlank()) {
- if (mediaFlags.isMediaTitleRequired(sbn.packageName, sbn.user)) {
- // App is required to provide a title: cancel the underlying notification
- try {
- statusBarService.onNotificationError(
- sbn.packageName,
- sbn.tag,
- sbn.id,
- sbn.uid,
- sbn.initialPid,
- MEDIA_TITLE_ERROR_MESSAGE,
- sbn.user.identifier
- )
- } catch (e: RemoteException) {
- Log.e(TAG, "cancelNotification failed: $e")
- }
- // Only add log for media removed if active media is updated with invalid title.
- foregroundExecutor.execute { removeEntry(key, !isNewlyActiveEntry) }
- return
- } else {
- // For apps that don't have the title requirement yet, add a placeholder
- song = context.getString(R.string.controls_media_empty_title, appName)
+ // For apps that don't include a title, log and add a placeholder
+ song = context.getString(R.string.controls_media_empty_title, appName)
+ try {
+ statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier)
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 3751c60..9bc66f6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -64,9 +64,4 @@
/** Check whether we allow remote media to generate resume controls */
fun isRemoteResumeAllowed() = featureFlags.isEnabled(Flags.MEDIA_REMOTE_RESUME)
-
- /** Check whether app is required to provide a non-empty media title */
- fun isMediaTitleRequired(packageName: String, user: UserHandle): Boolean {
- return StatusBarManager.isMediaTitleRequiredForApp(packageName, user)
- }
}
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/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
index 993c3801..b956207 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -354,7 +354,11 @@
@Override
protected void snapChild(final View animView, final float targetLeft, float velocity) {
- superSnapChild(animView, targetLeft, velocity);
+ if (animView instanceof SwipeableView) {
+ // only perform the snapback animation on views that are swipeable inside the shade.
+ superSnapChild(animView, targetLeft, velocity);
+ }
+
mCallback.onDragCancelled(animView);
if (targetLeft == 0) {
handleMenuCoveredOrDismissed();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index a6b2bd8..f26a84b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -30,6 +30,8 @@
import android.view.ViewDebug;
import android.view.WindowInsetsController.Appearance;
+import androidx.annotation.NonNull;
+
import com.android.internal.colorextraction.ColorExtractor.GradientColors;
import com.android.internal.view.AppearanceRegion;
import com.android.systemui.Dumpable;
@@ -46,7 +48,6 @@
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Date;
import javax.inject.Inject;
@@ -57,7 +58,8 @@
public class LightBarController implements BatteryController.BatteryStateChangeCallback, Dumpable {
private static final String TAG = "LightBarController";
- private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean DEBUG_NAVBAR = Compile.IS_DEBUG;
+ private static final boolean DEBUG_LOGS = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
private static final float NAV_BAR_INVERSION_SCRIM_ALPHA_THRESHOLD = 0.1f;
@@ -113,6 +115,7 @@
private String mLastSetScrimStateLog;
private String mLastNavigationBarAppearanceChangedLog;
+ private StringBuilder mLogStringBuilder = null;
@Inject
public LightBarController(
@@ -193,35 +196,43 @@
final boolean darkForTop = darkForQs || mGlobalActionsVisible;
mNavigationLight =
((mHasLightNavigationBar && !darkForScrim) || lightForScrim) && !darkForTop;
- mLastNavigationBarAppearanceChangedLog = "onNavigationBarAppearanceChanged()"
- + " appearance=" + appearance
- + " nbModeChanged=" + nbModeChanged
- + " navigationBarMode=" + navigationBarMode
- + " navbarColorManagedByIme=" + navbarColorManagedByIme
- + " mHasLightNavigationBar=" + mHasLightNavigationBar
- + " ignoreScrimForce=" + ignoreScrimForce
- + " darkForScrim=" + darkForScrim
- + " lightForScrim=" + lightForScrim
- + " darkForQs=" + darkForQs
- + " darkForTop=" + darkForTop
- + " mNavigationLight=" + mNavigationLight
- + " last=" + last
- + " timestamp=" + new Date();
- if (DEBUG) Log.d(TAG, mLastNavigationBarAppearanceChangedLog);
+ if (DEBUG_NAVBAR) {
+ mLastNavigationBarAppearanceChangedLog = getLogStringBuilder()
+ .append("onNavigationBarAppearanceChanged()")
+ .append(" appearance=").append(appearance)
+ .append(" nbModeChanged=").append(nbModeChanged)
+ .append(" navigationBarMode=").append(navigationBarMode)
+ .append(" navbarColorManagedByIme=").append(navbarColorManagedByIme)
+ .append(" mHasLightNavigationBar=").append(mHasLightNavigationBar)
+ .append(" ignoreScrimForce=").append(ignoreScrimForce)
+ .append(" darkForScrim=").append(darkForScrim)
+ .append(" lightForScrim=").append(lightForScrim)
+ .append(" darkForQs=").append(darkForQs)
+ .append(" darkForTop=").append(darkForTop)
+ .append(" mNavigationLight=").append(mNavigationLight)
+ .append(" last=").append(last)
+ .append(" timestamp=").append(System.currentTimeMillis())
+ .toString();
+ if (DEBUG_LOGS) Log.d(TAG, mLastNavigationBarAppearanceChangedLog);
+ }
} else {
mNavigationLight = mHasLightNavigationBar
&& (mDirectReplying && mNavbarColorManagedByIme || !mForceDarkForScrim)
&& !mQsCustomizing;
- mLastNavigationBarAppearanceChangedLog = "onNavigationBarAppearanceChanged()"
- + " appearance=" + appearance
- + " nbModeChanged=" + nbModeChanged
- + " navigationBarMode=" + navigationBarMode
- + " navbarColorManagedByIme=" + navbarColorManagedByIme
- + " mHasLightNavigationBar=" + mHasLightNavigationBar
- + " mNavigationLight=" + mNavigationLight
- + " last=" + last
- + " timestamp=" + new Date();
- if (DEBUG) Log.d(TAG, mLastNavigationBarAppearanceChangedLog);
+ if (DEBUG_NAVBAR) {
+ mLastNavigationBarAppearanceChangedLog = getLogStringBuilder()
+ .append("onNavigationBarAppearanceChanged()")
+ .append(" appearance=").append(appearance)
+ .append(" nbModeChanged=").append(nbModeChanged)
+ .append(" navigationBarMode=").append(navigationBarMode)
+ .append(" navbarColorManagedByIme=").append(navbarColorManagedByIme)
+ .append(" mHasLightNavigationBar=").append(mHasLightNavigationBar)
+ .append(" mNavigationLight=").append(mNavigationLight)
+ .append(" last=").append(last)
+ .append(" timestamp=").append(System.currentTimeMillis())
+ .toString();
+ if (DEBUG_LOGS) Log.d(TAG, mLastNavigationBarAppearanceChangedLog);
+ }
}
if (mNavigationLight != last) {
updateNavigation();
@@ -319,18 +330,22 @@
} else {
if (mForceLightForScrim != forceLightForScrimLast) reevaluate();
}
- mLastSetScrimStateLog = "setScrimState()"
- + " scrimState=" + scrimState
- + " scrimBehindAlpha=" + scrimBehindAlpha
- + " scrimInFrontColor=" + scrimInFrontColor
- + " forceForScrim=" + forceForScrim
- + " scrimColorIsLight=" + scrimColorIsLight
- + " mHasLightNavigationBar=" + mHasLightNavigationBar
- + " mBouncerVisible=" + mBouncerVisible
- + " mForceDarkForScrim=" + mForceDarkForScrim
- + " mForceLightForScrim=" + mForceLightForScrim
- + " timestamp=" + new Date();
- if (DEBUG) Log.d(TAG, mLastSetScrimStateLog);
+ if (DEBUG_NAVBAR) {
+ mLastSetScrimStateLog = getLogStringBuilder()
+ .append("setScrimState()")
+ .append(" scrimState=").append(scrimState)
+ .append(" scrimBehindAlpha=").append(scrimBehindAlpha)
+ .append(" scrimInFrontColor=").append(scrimInFrontColor)
+ .append(" forceForScrim=").append(forceForScrim)
+ .append(" scrimColorIsLight=").append(scrimColorIsLight)
+ .append(" mHasLightNavigationBar=").append(mHasLightNavigationBar)
+ .append(" mBouncerVisible=").append(mBouncerVisible)
+ .append(" mForceDarkForScrim=").append(mForceDarkForScrim)
+ .append(" mForceLightForScrim=").append(mForceLightForScrim)
+ .append(" timestamp=").append(System.currentTimeMillis())
+ .toString();
+ if (DEBUG_LOGS) Log.d(TAG, mLastSetScrimStateLog);
+ }
} else {
boolean forceDarkForScrimLast = mForceDarkForScrim;
// For BOUNCER/BOUNCER_SCRIMMED cases, we assume that alpha is always below threshold.
@@ -344,17 +359,30 @@
if (mHasLightNavigationBar && (mForceDarkForScrim != forceDarkForScrimLast)) {
reevaluate();
}
- mLastSetScrimStateLog = "setScrimState()"
- + " scrimState=" + scrimState
- + " scrimBehindAlpha=" + scrimBehindAlpha
- + " scrimInFrontColor=" + scrimInFrontColor
- + " mHasLightNavigationBar=" + mHasLightNavigationBar
- + " mForceDarkForScrim=" + mForceDarkForScrim
- + " timestamp=" + new Date();
- if (DEBUG) Log.d(TAG, mLastSetScrimStateLog);
+ if (DEBUG_NAVBAR) {
+ mLastSetScrimStateLog = getLogStringBuilder()
+ .append("setScrimState()")
+ .append(" scrimState=").append(scrimState)
+ .append(" scrimBehindAlpha=").append(scrimBehindAlpha)
+ .append(" scrimInFrontColor=").append(scrimInFrontColor)
+ .append(" mHasLightNavigationBar=").append(mHasLightNavigationBar)
+ .append(" mForceDarkForScrim=").append(mForceDarkForScrim)
+ .append(" timestamp=").append(System.currentTimeMillis())
+ .toString();
+ if (DEBUG_LOGS) Log.d(TAG, mLastSetScrimStateLog);
+ }
}
}
+ @NonNull
+ private StringBuilder getLogStringBuilder() {
+ if (mLogStringBuilder == null) {
+ mLogStringBuilder = new StringBuilder();
+ }
+ mLogStringBuilder.setLength(0);
+ return mLogStringBuilder;
+ }
+
private static boolean isLight(int appearance, int barMode, int flag) {
final boolean isTransparentBar = (barMode == MODE_TRANSPARENT
|| barMode == MODE_LIGHTS_OUT_TRANSPARENT);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 5ba02fa..b24a692 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);
}
@@ -464,31 +404,15 @@
@Override
public void destroy() {
+ Log.d(TAG, "destroy() called");
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
@@ -542,6 +466,7 @@
}
private void initDialog(int lockTaskModeState) {
+ Log.d(TAG, "initDialog: called!");
mDialog = new CustomDialog(mContext);
initDimens();
@@ -699,7 +624,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 +654,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);
@@ -1344,7 +1271,7 @@
}
protected void tryToRemoveCaptionsTooltip() {
- if (mHasSeenODICaptionsTooltip && mODICaptionsTooltipView != null) {
+ if (mHasSeenODICaptionsTooltip && mODICaptionsTooltipView != null && mDialog != null) {
ViewGroup container = mDialog.findViewById(R.id.volume_dialog_container);
container.removeView(mODICaptionsTooltipView);
mODICaptionsTooltipView = null;
@@ -1551,8 +1478,16 @@
mHandler.removeMessages(H.DISMISS);
mHandler.removeMessages(H.SHOW);
- if (mIsAnimatingDismiss) {
- Log.d(TAG, "dismissH: isAnimatingDismiss");
+
+ boolean showingStateInconsistent = !mShowing && mDialog != null && mDialog.isShowing();
+ // If incorrectly assuming dialog is not showing, continue and make the state consistent.
+ if (showingStateInconsistent) {
+ Log.d(TAG, "dismissH: volume dialog possible in inconsistent state:"
+ + "mShowing=" + mShowing + ", mDialog==null?" + (mDialog == null));
+ }
+ if (mIsAnimatingDismiss && !showingStateInconsistent) {
+ Log.d(TAG, "dismissH: skipping dismiss because isAnimatingDismiss is true"
+ + " and showingStateInconsistent is false");
Trace.endSection();
return;
}
@@ -1570,8 +1505,12 @@
.setDuration(mDialogHideAnimationDurationMs)
.setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
.withEndAction(() -> mHandler.postDelayed(() -> {
- mController.notifyVisible(false);
- mDialog.dismiss();
+ if (mController != null) {
+ mController.notifyVisible(false);
+ }
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
tryToRemoveCaptionsTooltip();
mIsAnimatingDismiss = 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/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 548d26f..78a65a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -98,7 +98,8 @@
)
testScope.runCurrent()
verify(globalWindowManager, times(1))
- .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
+ .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+ verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
}
@Test
@@ -115,7 +116,8 @@
)
testScope.runCurrent()
verify(globalWindowManager, times(1))
- .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
+ .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+ verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
}
@Test
@@ -161,7 +163,8 @@
keyguardRepository.setDozeAmount(1f)
testScope.runCurrent()
verify(globalWindowManager, times(1))
- .trimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
+ .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+ verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index 3bcefcf..56698e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -41,7 +41,6 @@
import androidx.media.utils.MediaConstants
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
-import com.android.internal.statusbar.IStatusBarService
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.R
@@ -133,7 +132,6 @@
@Mock lateinit var activityStarter: ActivityStarter
@Mock lateinit var smartspaceManager: SmartspaceManager
@Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- @Mock lateinit var statusBarService: IStatusBarService
lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
@Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
@Mock private lateinit var mediaRecommendationItem: SmartspaceAction
@@ -197,7 +195,6 @@
logger = logger,
smartspaceManager = smartspaceManager,
keyguardUpdateMonitor = keyguardUpdateMonitor,
- statusBarService = statusBarService,
)
verify(tunerService)
.addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
@@ -522,143 +519,12 @@
}
@Test
- fun testOnNotificationAdded_emptyTitle_isRequired_notLoaded() {
- // When the manager has a notification with an empty title, and the app is required
- // to include a non-empty title
- whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
- whenever(controller.metadata)
- .thenReturn(
- metadataBuilder
- .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE)
- .build()
- )
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-
- // Then the media control is not added and we report a notification error
- assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
- assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
- verify(statusBarService)
- .onNotificationError(
- eq(PACKAGE_NAME),
- eq(mediaNotification.tag),
- eq(mediaNotification.id),
- eq(mediaNotification.uid),
- eq(mediaNotification.initialPid),
- eq(MEDIA_TITLE_ERROR_MESSAGE),
- eq(mediaNotification.user.identifier)
- )
- verify(listener, never())
- .onMediaDataLoaded(
- eq(KEY),
- eq(null),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false)
- )
- verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
- verify(logger, never()).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), any())
- }
-
- @Test
- fun testOnNotificationAdded_blankTitle_isRequired_notLoaded() {
- // When the manager has a notification with a blank title, and the app is required
- // to include a non-empty title
- whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
- whenever(controller.metadata)
- .thenReturn(
- metadataBuilder
- .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
- .build()
- )
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-
- // Then the media control is not added and we report a notification error
- assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
- assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
- verify(statusBarService)
- .onNotificationError(
- eq(PACKAGE_NAME),
- eq(mediaNotification.tag),
- eq(mediaNotification.id),
- eq(mediaNotification.uid),
- eq(mediaNotification.initialPid),
- eq(MEDIA_TITLE_ERROR_MESSAGE),
- eq(mediaNotification.user.identifier)
- )
- verify(listener, never())
- .onMediaDataLoaded(
- eq(KEY),
- eq(null),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false)
- )
- verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any())
- verify(logger, never()).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), any())
- }
-
- @Test
- fun testOnNotificationUpdated_invalidTitle_isRequired_logMediaRemoved() {
- // When the app is required to provide a non-blank title, and updates a previously valid
- // title to an empty one
- whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
- addNotificationAndLoad()
- val data = mediaDataCaptor.value
-
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(null),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false)
- )
-
- reset(listener)
- whenever(controller.metadata)
- .thenReturn(
- metadataBuilder
- .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE)
- .build()
- )
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
-
- // Then the media control is removed
- assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
- assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
- verify(statusBarService)
- .onNotificationError(
- eq(PACKAGE_NAME),
- eq(mediaNotification.tag),
- eq(mediaNotification.id),
- eq(mediaNotification.uid),
- eq(mediaNotification.initialPid),
- eq(MEDIA_TITLE_ERROR_MESSAGE),
- eq(mediaNotification.user.identifier)
- )
- verify(listener, never())
- .onMediaDataLoaded(
- eq(KEY),
- eq(null),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false)
- )
- verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
- }
-
- @Test
- fun testOnNotificationAdded_emptyTitle_notRequired_hasPlaceholder() {
+ fun testOnNotificationAdded_emptyTitle_hasPlaceholder() {
// When the manager has a notification with an empty title, and the app is not
// required to include a non-empty title
val mockPackageManager = mock(PackageManager::class.java)
context.setMockPackageManager(mockPackageManager)
whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
- whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(false)
whenever(controller.metadata)
.thenReturn(
metadataBuilder
@@ -684,13 +550,12 @@
}
@Test
- fun testOnNotificationAdded_blankTitle_notRequired_hasPlaceholder() {
+ fun testOnNotificationAdded_blankTitle_hasPlaceholder() {
// GIVEN that the manager has a notification with a blank title, and the app is not
// required to include a non-empty title
val mockPackageManager = mock(PackageManager::class.java)
context.setMockPackageManager(mockPackageManager)
whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
- whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(false)
whenever(controller.metadata)
.thenReturn(
metadataBuilder
@@ -722,7 +587,6 @@
val mockPackageManager = mock(PackageManager::class.java)
context.setMockPackageManager(mockPackageManager)
whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME)
- whenever(mediaFlags.isMediaTitleRequired(any(), any())).thenReturn(true)
whenever(controller.metadata)
.thenReturn(
metadataBuilder
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
index 2eca78a..e92368d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
@@ -19,6 +19,7 @@
import android.testing.AndroidTestingRunner
import android.view.LayoutInflater
import androidx.test.filters.SmallTest
+import com.android.app.animation.Interpolators
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.TextAnimator
@@ -64,8 +65,8 @@
color = 200,
strokeWidth = -1F,
animate = false,
- duration = 350L,
- interpolator = null,
+ duration = 833L,
+ interpolator = Interpolators.EMPHASIZED_DECELERATE,
delay = 0L,
onAnimationEnd = null
)
@@ -98,8 +99,8 @@
color = 200,
strokeWidth = -1F,
animate = true,
- duration = 350L,
- interpolator = null,
+ duration = 833L,
+ interpolator = Interpolators.EMPHASIZED_DECELERATE,
delay = 0L,
onAnimationEnd = null
)
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/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 551499e..7632d01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -392,23 +392,32 @@
@Test
public void testSnapchild_targetIsZero() {
- doNothing().when(mSwipeHelper).superSnapChild(mView, 0, 0);
- mSwipeHelper.snapChild(mView, 0, 0);
+ doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 0, 0);
+ mSwipeHelper.snapChild(mNotificationRow, 0, 0);
- verify(mCallback, times(1)).onDragCancelled(mView);
- verify(mSwipeHelper, times(1)).superSnapChild(mView, 0, 0);
+ verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
+ verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 0, 0);
verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed();
}
@Test
public void testSnapchild_targetNotZero() {
+ doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 10, 0);
+ mSwipeHelper.snapChild(mNotificationRow, 10, 0);
+
+ verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
+ verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 10, 0);
+ verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed();
+ }
+
+ @Test
+ public void testSnapchild_targetNotSwipeable() {
doNothing().when(mSwipeHelper).superSnapChild(mView, 10, 0);
mSwipeHelper.snapChild(mView, 10, 0);
- verify(mCallback, times(1)).onDragCancelled(mView);
- verify(mSwipeHelper, times(1)).superSnapChild(mView, 10, 0);
- verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed();
+ verify(mCallback).onDragCancelled(mView);
+ verify(mSwipeHelper, never()).superSnapChild(mView, 10, 0);
}
@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/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 1e5f187..85a0185 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -837,7 +837,7 @@
*/
@GuardedBy("mService")
void enqueueOomAdjTargetLocked(ProcessRecord app) {
- if (app != null) {
+ if (app != null && app.mState.getMaxAdj() > FOREGROUND_APP_ADJ) {
mPendingProcessSet.add(app);
}
}
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..d0b6cdc 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
@@ -1157,6 +1145,11 @@
MAX_STREAM_VOLUME[AudioSystem.STREAM_SYSTEM];
}
+ int minAssistantVolume = SystemProperties.getInt("ro.config.assistant_vol_min", -1);
+ if (minAssistantVolume != -1) {
+ MIN_STREAM_VOLUME[AudioSystem.STREAM_ASSISTANT] = minAssistantVolume;
+ }
+
// Read following properties to configure max volume (number of steps) and default volume
// for STREAM_NOTIFICATION and STREAM_RING:
// config_audio_notif_vol_default
@@ -1277,22 +1270,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/biometrics/TEST_MAPPING b/services/core/java/com/android/server/biometrics/TEST_MAPPING
index daca00b..9e60ba8 100644
--- a/services/core/java/com/android/server/biometrics/TEST_MAPPING
+++ b/services/core/java/com/android/server/biometrics/TEST_MAPPING
@@ -6,24 +6,5 @@
{
"name": "CtsBiometricsHostTestCases"
}
- ],
- "ironwood-postsubmit": [
- {
- "name": "BiometricsE2eTests",
- "options": [
- {
- "include-annotation": "android.platform.test.annotations.IwTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- },
- {
- "include-filter": "android.platform.test.scenario.biometrics"
- },
- {
- "exclude-annotation": "android.platform.test.annotations.FlakyTest"
- }
- ]
- }
- ]
+ ]
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
index a486d16..694dfd2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/UserAwareBiometricScheduler.java
@@ -70,9 +70,11 @@
// Set mStopUserClient to null when StopUserClient fails. Otherwise it's possible
// for that the queue will wait indefinitely until the field is cleared.
- if (clientMonitor instanceof StopUserClient<?> && !success) {
- Slog.w(getTag(),
- "StopUserClient failed(), is the HAL stuck? Clearing mStopUserClient");
+ if (clientMonitor instanceof StopUserClient<?>) {
+ if (!success) {
+ Slog.w(getTag(), "StopUserClient failed(), is the HAL stuck? "
+ + "Clearing mStopUserClient");
+ }
mStopUserClient = null;
}
if (mCurrentOperation != null && mCurrentOperation.isFor(mOwner)) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
index ffbf4e1..2ad41c2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/Sensor.java
@@ -89,7 +89,7 @@
@NonNull private final Map<Integer, Long> mAuthenticatorIds;
@NonNull private final Supplier<AidlSession> mLazySession;
- @Nullable private AidlSession mCurrentSession;
+ @Nullable AidlSession mCurrentSession;
@VisibleForTesting
public static class HalSessionCallback extends ISessionCallback.Stub {
@@ -486,7 +486,7 @@
Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
@NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
- @NonNull BiometricContext biometricContext) {
+ @NonNull BiometricContext biometricContext, AidlSession session) {
mTag = tag;
mProvider = provider;
mContext = context;
@@ -549,6 +549,14 @@
mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
}
+ Sensor(@NonNull String tag, @NonNull FaceProvider provider, @NonNull Context context,
+ @NonNull Handler handler, @NonNull FaceSensorPropertiesInternal sensorProperties,
+ @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+ @NonNull BiometricContext biometricContext) {
+ this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
+ biometricContext, null);
+ }
+
@NonNull Supplier<AidlSession> getLazySession() {
return mLazySession;
}
@@ -557,7 +565,7 @@
return mSensorProperties;
}
- @Nullable AidlSession getSessionForUser(int userId) {
+ @VisibleForTesting @Nullable AidlSession getSessionForUser(int userId) {
if (mCurrentSession != null && mCurrentSession.getUserId() == userId) {
return mCurrentSession;
} else {
@@ -641,6 +649,8 @@
BiometricsProtoEnums.MODALITY_FACE,
BiometricsProtoEnums.ISSUE_HAL_DEATH,
-1 /* sensorId */);
+ } else if (client != null) {
+ client.cancel();
}
mScheduler.recordCrashState();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
index c0dde72..56b85ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
@@ -90,7 +90,7 @@
@NonNull private final LockoutCache mLockoutCache;
@NonNull private final Map<Integer, Long> mAuthenticatorIds;
- @Nullable private AidlSession mCurrentSession;
+ @Nullable AidlSession mCurrentSession;
@NonNull private final Supplier<AidlSession> mLazySession;
@VisibleForTesting
@@ -439,7 +439,7 @@
@NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
- @NonNull BiometricContext biometricContext) {
+ @NonNull BiometricContext biometricContext, AidlSession session) {
mTag = tag;
mProvider = provider;
mContext = context;
@@ -501,6 +501,16 @@
});
mAuthenticatorIds = new HashMap<>();
mLazySession = () -> mCurrentSession != null ? mCurrentSession : null;
+ mCurrentSession = session;
+ }
+
+ Sensor(@NonNull String tag, @NonNull FingerprintProvider provider, @NonNull Context context,
+ @NonNull Handler handler, @NonNull FingerprintSensorPropertiesInternal sensorProperties,
+ @NonNull LockoutResetDispatcher lockoutResetDispatcher,
+ @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher,
+ @NonNull BiometricContext biometricContext) {
+ this(tag, provider, context, handler, sensorProperties, lockoutResetDispatcher,
+ gestureAvailabilityDispatcher, biometricContext, null);
}
@NonNull Supplier<AidlSession> getLazySession() {
@@ -599,6 +609,8 @@
BiometricsProtoEnums.MODALITY_FINGERPRINT,
BiometricsProtoEnums.ISSUE_HAL_DEATH,
-1 /* sensorId */);
+ } else if (client != null) {
+ client.cancel();
}
mScheduler.recordCrashState();
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/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java
index e8c65ef..d550650 100644
--- a/services/core/java/com/android/server/display/BrightnessTracker.java
+++ b/services/core/java/com/android/server/display/BrightnessTracker.java
@@ -796,6 +796,7 @@
pw.print(", isUserSetBrightness=" + events[i].isUserSetBrightness);
pw.print(", powerBrightnessFactor=" + events[i].powerBrightnessFactor);
pw.print(", isDefaultBrightnessConfig=" + events[i].isDefaultBrightnessConfig);
+ pw.print(", recent lux values=");
pw.print(" {");
for (int j = 0; j < events[i].luxValues.length; ++j){
if (j != 0) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 1674141..41e4671d 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -157,6 +157,7 @@
private static final int REPORTED_TO_POLICY_SCREEN_TURNING_OFF = 3;
private static final int RINGBUFFER_MAX = 100;
+ private static final int RINGBUFFER_RBC_MAX = 20;
private static final float[] BRIGHTNESS_RANGE_BOUNDARIES = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80,
@@ -390,6 +391,10 @@
// Keeps a record of brightness changes for dumpsys.
private RingBuffer<BrightnessEvent> mBrightnessEventRingBuffer;
+ // Keeps a record of rbc changes for dumpsys.
+ private final RingBuffer<BrightnessEvent> mRbcEventRingBuffer =
+ new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_RBC_MAX);
+
// Controls and tracks all the wakelocks that are acquired/released by the system. Also acts as
// a medium of communication between this class and the PowerManagerService.
private final WakelockController mWakelockController;
@@ -1593,6 +1598,10 @@
mTempBrightnessEvent.getReason().getReason() == BrightnessReason.REASON_TEMPORARY
&& mLastBrightnessEvent.getReason().getReason()
== BrightnessReason.REASON_TEMPORARY;
+ // Purely for dumpsys;
+ final boolean isRbcEvent =
+ mLastBrightnessEvent.isRbcEnabled() != mTempBrightnessEvent.isRbcEnabled();
+
if ((!mTempBrightnessEvent.equalsMainData(mLastBrightnessEvent) && !tempToTempTransition)
|| brightnessAdjustmentFlags != 0) {
mTempBrightnessEvent.setInitialBrightness(mLastBrightnessEvent.getBrightness());
@@ -1612,6 +1621,10 @@
if (mBrightnessEventRingBuffer != null) {
mBrightnessEventRingBuffer.append(newEvent);
}
+ if (isRbcEvent) {
+ mRbcEventRingBuffer.append(newEvent);
+ }
+
}
// Update display white-balance.
@@ -2359,6 +2372,8 @@
dumpBrightnessEvents(pw);
}
+ dumpRbcEvents(pw);
+
if (mHbmController != null) {
mHbmController.dump(pw);
}
@@ -2431,6 +2446,20 @@
}
}
+ private void dumpRbcEvents(PrintWriter pw) {
+ int size = mRbcEventRingBuffer.size();
+ if (size < 1) {
+ pw.println("No Reduce Bright Colors Adjustments");
+ return;
+ }
+
+ pw.println("Reduce Bright Colors Adjustments Last " + size + " Events: ");
+ BrightnessEvent[] eventArray = mRbcEventRingBuffer.toArray();
+ for (int i = 0; i < mRbcEventRingBuffer.size(); i++) {
+ pw.println(" " + eventArray[i]);
+ }
+ }
+
private void noteScreenState(int screenState) {
// Log screen state change with display id
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index cede273..be9df4a 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -63,10 +63,12 @@
import android.hardware.hdmi.IHdmiVendorCommandListener;
import android.hardware.tv.cec.V1_0.SendMessageResult;
import android.media.AudioAttributes;
+import android.media.AudioDescriptor;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioDeviceVolumeManager;
import android.media.AudioManager;
+import android.media.AudioProfile;
import android.media.VolumeInfo;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
@@ -4727,9 +4729,22 @@
// reported connection state changes, but even if it did, it won't take effect.
if (mEarcLocalDevice != null) {
mEarcLocalDevice.handleEarcStateChange(status);
+ } else if (status == HDMI_EARC_STATUS_ARC_PENDING) {
+ // If the local device is null we notify the Audio Service that eARC connection
+ // is disabled.
+ notifyEarcStatusToAudioService(false, new ArrayList<>());
+ startArcAction(true, null);
}
}
+ protected void notifyEarcStatusToAudioService(
+ boolean enabled, List<AudioDescriptor> audioDescriptors) {
+ AudioDeviceAttributes attributes = new AudioDeviceAttributes(
+ AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, "", "",
+ new ArrayList<AudioProfile>(), audioDescriptors);
+ getAudioManager().setWiredDeviceConnectionState(attributes, enabled ? 1 : 0);
+ }
+
@ServiceThreadOnly
void handleEarcCapabilitiesReported(byte[] rawCapabilities, int portId) {
assertRunOnServiceThread();
diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
index 9058c98..873d5fc 100644
--- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
+++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
@@ -23,8 +23,6 @@
import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.AudioDescriptor;
-import android.media.AudioDeviceAttributes;
-import android.media.AudioDeviceInfo;
import android.media.AudioProfile;
import android.os.Handler;
import android.util.IndentingPrintWriter;
@@ -88,10 +86,10 @@
mReportCapsHandler.removeCallbacksAndMessages(null);
if (status == HDMI_EARC_STATUS_IDLE) {
- notifyEarcStatusToAudioService(false, new ArrayList<>());
+ mService.notifyEarcStatusToAudioService(false, new ArrayList<>());
mService.startArcAction(false, null);
} else if (status == HDMI_EARC_STATUS_ARC_PENDING) {
- notifyEarcStatusToAudioService(false, new ArrayList<>());
+ mService.notifyEarcStatusToAudioService(false, new ArrayList<>());
mService.startArcAction(true, null);
} else if (status == HDMI_EARC_STATUS_EARC_PENDING
&& oldEarcStatus == HDMI_EARC_STATUS_ARC_PENDING) {
@@ -110,19 +108,11 @@
&& mReportCapsHandler.hasCallbacks(mReportCapsRunnable)) {
mReportCapsHandler.removeCallbacksAndMessages(null);
List<AudioDescriptor> audioDescriptors = parseCapabilities(rawCapabilities);
- notifyEarcStatusToAudioService(true, audioDescriptors);
+ mService.notifyEarcStatusToAudioService(true, audioDescriptors);
}
}
}
- private void notifyEarcStatusToAudioService(
- boolean enabled, List<AudioDescriptor> audioDescriptors) {
- AudioDeviceAttributes attributes = new AudioDeviceAttributes(
- AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI_EARC, "", "",
- new ArrayList<AudioProfile>(), audioDescriptors);
- mService.getAudioManager().setWiredDeviceConnectionState(attributes, enabled ? 1 : 0);
- }
-
/**
* Runnable for waiting for a certain amount of time for the audio system to report its
* capabilities after eARC was connected. If the audio system doesn´t report its capabilities in
@@ -134,7 +124,7 @@
public void run() {
synchronized (mLock) {
if (mEarcStatus == HDMI_EARC_STATUS_EARC_CONNECTED) {
- notifyEarcStatusToAudioService(true, new ArrayList<>());
+ mService.notifyEarcStatusToAudioService(true, new ArrayList<>());
}
}
}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 20f0697..2e62ef4 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -889,22 +889,31 @@
}
- private void migrateOldDataAfterSystemReady() {
- // Migrate the FRP credential to the persistent data block
+ @VisibleForTesting
+ void migrateOldDataAfterSystemReady() {
+ // Write the FRP persistent data block if needed.
+ //
+ // The original purpose of this code was to write the FRP block for the first time, when
+ // upgrading from Android 8.1 or earlier which didn't use the FRP block. This code has
+ // since been repurposed to also fix the "bad" (non-forwards-compatible) FRP block written
+ // by Android 14 Beta 2. For this reason, the database key used here has been renamed from
+ // "migrated_frp" to "migrated_frp2" to cause migrateFrpCredential() to run again on devices
+ // where it had run before.
if (LockPatternUtils.frpCredentialEnabled(mContext)
- && !getBoolean("migrated_frp", false, 0)) {
+ && !getBoolean("migrated_frp2", false, 0)) {
migrateFrpCredential();
- setBoolean("migrated_frp", true, 0);
+ setBoolean("migrated_frp2", true, 0);
}
}
/**
- * Migrate the credential for the FRP credential owner user if the following are satisfied:
- * - the user has a secure credential
- * - the FRP credential is not set up
+ * Write the FRP persistent data block if the following are satisfied:
+ * - the user who owns the FRP credential has a nonempty credential
+ * - the FRP persistent data block doesn't exist or uses the "bad" format from Android 14 Beta 2
*/
private void migrateFrpCredential() {
- if (mStorage.readPersistentDataBlock() != PersistentData.NONE) {
+ PersistentData data = mStorage.readPersistentDataBlock();
+ if (data != PersistentData.NONE && !data.isBadFormatFromAndroid14Beta()) {
return;
}
for (UserInfo userInfo : mUserManager.getUsers()) {
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index 731ecad..2fa637e 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -606,6 +606,11 @@
this.payload = payload;
}
+ public boolean isBadFormatFromAndroid14Beta() {
+ return (this.type == TYPE_SP_GATEKEEPER || this.type == TYPE_SP_WEAVER)
+ && SyntheticPasswordManager.PasswordData.isBadFormatFromAndroid14Beta(this.payload);
+ }
+
public static PersistentData fromBytes(byte[] frpData) {
if (frpData == null || frpData.length == 0) {
return NONE;
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 65e7a00..66f862a 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -370,6 +370,15 @@
return result;
}
+ /**
+ * Returns true if the given serialized PasswordData begins with the value 2 as a short.
+ * This detects the "bad" (non-forwards-compatible) PasswordData format that was temporarily
+ * used during development of Android 14. For more details, see fromBytes() below.
+ */
+ public static boolean isBadFormatFromAndroid14Beta(byte[] data) {
+ return data != null && data.length >= 2 && data[0] == 0 && data[1] == 2;
+ }
+
public static PasswordData fromBytes(byte[] data) {
PasswordData result = new PasswordData();
ByteBuffer buffer = ByteBuffer.allocate(data.length);
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/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 9add537..a079875 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -82,6 +82,7 @@
import android.os.IInterface;
import android.os.IRemoteCallback;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
@@ -2514,6 +2515,7 @@
* Propagate a wake event to the wallpaper engine(s).
*/
public void notifyWakingUp(int x, int y, @NonNull Bundle extras) {
+ checkCallerIsSystemOrSystemUi();
synchronized (mLock) {
if (mIsLockscreenLiveWallpaperEnabled) {
for (WallpaperData data : getActiveWallpapers()) {
@@ -2551,6 +2553,7 @@
* Propagate a sleep event to the wallpaper engine(s).
*/
public void notifyGoingToSleep(int x, int y, @NonNull Bundle extras) {
+ checkCallerIsSystemOrSystemUi();
synchronized (mLock) {
if (mIsLockscreenLiveWallpaperEnabled) {
for (WallpaperData data : getActiveWallpapers()) {
@@ -3684,6 +3687,14 @@
mActivityManager.getPackageImportance(callingPackage) == IMPORTANCE_FOREGROUND);
}
+ /** Check that the caller is either system_server or systemui */
+ private void checkCallerIsSystemOrSystemUi() {
+ if (Binder.getCallingUid() != Process.myUid() && mContext.checkCallingPermission(
+ android.Manifest.permission.STATUS_BAR_SERVICE) != PERMISSION_GRANTED) {
+ throw new SecurityException("Access denied: only system processes can call this");
+ }
+ }
+
/**
* Certain user types do not support wallpapers (e.g. managed profiles). The check is
* implemented through through the OP_WRITE_WALLPAPER AppOp.
diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
index 5c929a9..bd07622 100644
--- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
+++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java
@@ -30,6 +30,7 @@
import android.graphics.Rect;
import android.graphics.RenderNode;
import android.hardware.HardwareBuffer;
+import android.os.SystemClock;
import android.os.Trace;
import android.util.Pair;
import android.util.Slog;
@@ -213,6 +214,7 @@
// Failed to acquire image. Has been logged.
return null;
}
+ builder.setCaptureTime(SystemClock.elapsedRealtimeNanos());
builder.setSnapshot(screenshotBuffer.getHardwareBuffer());
builder.setColorSpace(screenshotBuffer.getColorSpace());
return builder.build();
@@ -432,6 +434,7 @@
// color above
return new TaskSnapshot(
System.currentTimeMillis() /* id */,
+ SystemClock.elapsedRealtimeNanos() /* captureTime */,
topActivity.mActivityComponent, hwBitmap.getHardwareBuffer(),
hwBitmap.getColorSpace(), mainWindow.getConfiguration().orientation,
mainWindow.getWindowConfiguration().getRotation(), new Point(taskWidth, taskHeight),
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/AppSnapshotLoader.java b/services/core/java/com/android/server/wm/AppSnapshotLoader.java
index 88c4752..ed65a2b 100644
--- a/services/core/java/com/android/server/wm/AppSnapshotLoader.java
+++ b/services/core/java/com/android/server/wm/AppSnapshotLoader.java
@@ -28,6 +28,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
+import android.os.SystemClock;
import android.util.Slog;
import android.window.TaskSnapshot;
@@ -195,8 +196,9 @@
taskSize = new Point(proto.taskWidth, proto.taskHeight);
}
- return new TaskSnapshot(proto.id, topActivityComponent, buffer,
- hwBitmap.getColorSpace(), proto.orientation, proto.rotation, taskSize,
+ return new TaskSnapshot(proto.id, SystemClock.elapsedRealtimeNanos(),
+ topActivityComponent, buffer, hwBitmap.getColorSpace(),
+ proto.orientation, proto.rotation, taskSize,
new Rect(proto.insetLeft, proto.insetTop, proto.insetRight, proto.insetBottom),
new Rect(proto.letterboxInsetLeft, proto.letterboxInsetTop,
proto.letterboxInsetRight, proto.letterboxInsetBottom),
diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java
index d22c38e..123a74d 100644
--- a/services/core/java/com/android/server/wm/AppWarnings.java
+++ b/services/core/java/com/android/server/wm/AppWarnings.java
@@ -28,11 +28,13 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.SystemProperties;
import android.util.AtomicFile;
import android.util.DisplayMetrics;
import android.util.Slog;
import android.util.Xml;
+import com.android.internal.util.ArrayUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
@@ -56,6 +58,7 @@
public static final int FLAG_HIDE_DISPLAY_SIZE = 0x01;
public static final int FLAG_HIDE_COMPILE_SDK = 0x02;
public static final int FLAG_HIDE_DEPRECATED_SDK = 0x04;
+ public static final int FLAG_HIDE_DEPRECATED_ABI = 0x08;
private final HashMap<String, Integer> mPackageFlags = new HashMap<>();
@@ -68,6 +71,7 @@
private UnsupportedDisplaySizeDialog mUnsupportedDisplaySizeDialog;
private UnsupportedCompileSdkDialog mUnsupportedCompileSdkDialog;
private DeprecatedTargetSdkVersionDialog mDeprecatedTargetSdkVersionDialog;
+ private DeprecatedAbiDialog mDeprecatedAbiDialog;
/** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */
private HashSet<ComponentName> mAlwaysShowUnsupportedCompileSdkWarningActivities =
@@ -166,6 +170,31 @@
}
/**
+ * Shows the "deprecated abi" warning, if necessary. This can only happen is the device
+ * supports both 64-bit and 32-bit ABIs, and the app only contains 32-bit libraries. The app
+ * cannot be installed if the device only supports 64-bit ABI while the app contains only 32-bit
+ * libraries.
+ *
+ * @param r activity record for which the warning may be displayed
+ */
+ public void showDeprecatedAbiDialogIfNeeded(ActivityRecord r) {
+ final boolean disableDeprecatedAbiDialog = SystemProperties.getBoolean(
+ "debug.wm.disable_deprecated_abi_dialog", false);
+ if (disableDeprecatedAbiDialog) {
+ return;
+ }
+ final String appPrimaryAbi = r.info.applicationInfo.primaryCpuAbi;
+ final String appSecondaryAbi = r.info.applicationInfo.secondaryCpuAbi;
+ final boolean appContainsOnly32bitLibraries =
+ (appPrimaryAbi != null && appSecondaryAbi == null && !appPrimaryAbi.contains("64"));
+ final boolean is64BitDevice =
+ ArrayUtils.find(Build.SUPPORTED_ABIS, abi -> abi.contains("64")) != null;
+ if (is64BitDevice && appContainsOnly32bitLibraries) {
+ mUiHandler.showDeprecatedAbiDialog(r);
+ }
+ }
+
+ /**
* Called when an activity is being started.
*
* @param r record for the activity being started
@@ -174,6 +203,7 @@
showUnsupportedCompileSdkDialogIfNeeded(r);
showUnsupportedDisplaySizeDialogIfNeeded(r);
showDeprecatedTargetDialogIfNeeded(r);
+ showDeprecatedAbiDialogIfNeeded(r);
}
/**
@@ -299,6 +329,27 @@
}
/**
+ * Shows the "deprecated abi" warning for the given application.
+ * <p>
+ * <strong>Note:</strong> Must be called on the UI thread.
+ *
+ * @param ar record for the activity that triggered the warning
+ */
+ @UiThread
+ private void showDeprecatedAbiDialogUiThread(ActivityRecord ar) {
+ if (mDeprecatedAbiDialog != null) {
+ mDeprecatedAbiDialog.dismiss();
+ mDeprecatedAbiDialog = null;
+ }
+ if (ar != null && !hasPackageFlag(
+ ar.packageName, FLAG_HIDE_DEPRECATED_ABI)) {
+ mDeprecatedAbiDialog = new DeprecatedAbiDialog(
+ AppWarnings.this, mUiContext, ar.info.applicationInfo);
+ mDeprecatedAbiDialog.show();
+ }
+ }
+
+ /**
* Dismisses all warnings for the given package.
* <p>
* <strong>Note:</strong> Must be called on the UI thread.
@@ -328,6 +379,13 @@
mDeprecatedTargetSdkVersionDialog.dismiss();
mDeprecatedTargetSdkVersionDialog = null;
}
+
+ // Hides the "deprecated abi" dialog if necessary.
+ if (mDeprecatedAbiDialog != null && (name == null || name.equals(
+ mDeprecatedAbiDialog.mPackageName))) {
+ mDeprecatedAbiDialog.dismiss();
+ mDeprecatedAbiDialog = null;
+ }
}
/**
@@ -381,6 +439,7 @@
private static final int MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG = 3;
private static final int MSG_HIDE_DIALOGS_FOR_PACKAGE = 4;
private static final int MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG = 5;
+ private static final int MSG_SHOW_DEPRECATED_ABI_DIALOG = 6;
public UiHandler(Looper looper) {
super(looper, null, true);
@@ -408,6 +467,10 @@
final ActivityRecord ar = (ActivityRecord) msg.obj;
showDeprecatedTargetSdkDialogUiThread(ar);
} break;
+ case MSG_SHOW_DEPRECATED_ABI_DIALOG: {
+ final ActivityRecord ar = (ActivityRecord) msg.obj;
+ showDeprecatedAbiDialogUiThread(ar);
+ } break;
}
}
@@ -431,6 +494,11 @@
obtainMessage(MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG, r).sendToTarget();
}
+ public void showDeprecatedAbiDialog(ActivityRecord r) {
+ removeMessages(MSG_SHOW_DEPRECATED_ABI_DIALOG);
+ obtainMessage(MSG_SHOW_DEPRECATED_ABI_DIALOG, r).sendToTarget();
+ }
+
public void hideDialogsForPackage(String name) {
obtainMessage(MSG_HIDE_DIALOGS_FOR_PACKAGE, name).sendToTarget();
}
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/DeprecatedAbiDialog.java b/services/core/java/com/android/server/wm/DeprecatedAbiDialog.java
new file mode 100644
index 0000000..e96208d
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DeprecatedAbiDialog.java
@@ -0,0 +1,58 @@
+/*
+ * 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.server.wm;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageItemInfo;
+import android.content.pm.PackageManager;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.android.internal.R;
+
+class DeprecatedAbiDialog extends AppWarnings.BaseDialog {
+ DeprecatedAbiDialog(final AppWarnings manager, Context context,
+ ApplicationInfo appInfo) {
+ super(manager, appInfo.packageName);
+
+ final PackageManager pm = context.getPackageManager();
+ final CharSequence label = appInfo.loadSafeLabel(pm,
+ PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX,
+ PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE
+ | PackageItemInfo.SAFE_LABEL_FLAG_TRIM);
+ final CharSequence message = context.getString(R.string.deprecated_abi_message);
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context)
+ .setPositiveButton(R.string.ok, (dialog, which) ->
+ manager.setPackageFlag(
+ mPackageName, AppWarnings.FLAG_HIDE_DEPRECATED_ABI, true))
+ .setMessage(message)
+ .setTitle(label);
+
+ // Ensure the content view is prepared.
+ mDialog = builder.create();
+ mDialog.create();
+
+ final Window window = mDialog.getWindow();
+ window.setType(WindowManager.LayoutParams.TYPE_PHONE);
+
+ // DO NOT MODIFY. Used by CTS to verify the dialog is displayed.
+ window.getAttributes().setTitle("DeprecatedAbiDialog");
+ }
+}
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 7e20b3b..c747c09 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -180,6 +180,18 @@
}
/**
+ * Returns the elapsed real time (in nanoseconds) at which a snapshot for the given task was
+ * last taken, or -1 if no such snapshot exists for that task.
+ */
+ long getSnapshotCaptureTime(int taskId) {
+ final TaskSnapshot snapshot = mCache.getSnapshot(taskId);
+ if (snapshot != null) {
+ return snapshot.getCaptureTime();
+ }
+ return -1;
+ }
+
+ /**
* @see WindowManagerInternal#clearSnapshotCache
*/
public void clearSnapshotCache() {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index c763cfa..663db86 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -382,11 +382,7 @@
// Add FLAG_ABOVE_TRANSIENT_LAUNCH to the tree of transient-hide tasks,
// so ChangeInfo#hasChanged() can return true to report the transition info.
for (int i = mChanges.size() - 1; i >= 0; --i) {
- final WindowContainer<?> wc = mChanges.keyAt(i);
- if (wc.asTaskFragment() == null && wc.asActivityRecord() == null) continue;
- if (isInTransientHide(wc)) {
- mChanges.valueAt(i).mFlags |= ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH;
- }
+ updateTransientFlags(mChanges.valueAt(i));
}
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Transition %d: Set %s as "
@@ -581,7 +577,9 @@
for (WindowContainer<?> curr = getAnimatableParent(wc);
curr != null && !mChanges.containsKey(curr);
curr = getAnimatableParent(curr)) {
- mChanges.put(curr, new ChangeInfo(curr));
+ final ChangeInfo info = new ChangeInfo(curr);
+ updateTransientFlags(info);
+ mChanges.put(curr, info);
if (isReadyGroup(curr)) {
mReadyTracker.addGroup(curr);
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for"
@@ -600,6 +598,7 @@
ChangeInfo info = mChanges.get(wc);
if (info == null) {
info = new ChangeInfo(wc);
+ updateTransientFlags(info);
mChanges.put(wc, info);
}
mParticipants.add(wc);
@@ -615,6 +614,14 @@
}
}
+ private void updateTransientFlags(@NonNull ChangeInfo info) {
+ final WindowContainer<?> wc = info.mContainer;
+ // Only look at tasks, taskfragments, or activities
+ if (wc.asTaskFragment() == null && wc.asActivityRecord() == null) return;
+ if (!isInTransientHide(wc)) return;
+ info.mFlags |= ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH;
+ }
+
private void recordDisplay(DisplayContent dc) {
if (dc == null || mTargetDisplays.contains(dc)) return;
mTargetDisplays.add(dc);
@@ -1079,12 +1086,23 @@
if (commitVisibility) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" Commit activity becoming invisible: %s", ar);
+ final SnapshotController snapController = mController.mSnapshotController;
if (mTransientLaunches != null && !task.isVisibleRequested()) {
+ final long startTimeNs = mLogger.mSendTimeNs;
+ final long lastSnapshotTimeNs = snapController.mTaskSnapshotController
+ .getSnapshotCaptureTime(task.mTaskId);
// If transition is transient, then snapshots are taken at end of
- // transition.
- mController.mSnapshotController.mTaskSnapshotController
- .recordSnapshot(task, false /* allowSnapshotHome */);
- mController.mSnapshotController.mActivitySnapshotController
+ // transition only if a snapshot was not already captured by request
+ // during the transition
+ if (lastSnapshotTimeNs < startTimeNs) {
+ snapController.mTaskSnapshotController
+ .recordSnapshot(task, false /* allowSnapshotHome */);
+ } else {
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+ " Skipping post-transition snapshot for task %d",
+ task.mTaskId);
+ }
+ snapController.mActivitySnapshotController
.notifyAppVisibilityChanged(ar, false /* visible */);
}
ar.commitVisibility(false /* visible */, false /* performLayout */,
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/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
index 25bd9bc..be9f52e 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/SensorTest.java
@@ -17,12 +17,14 @@
package com.android.server.biometrics.sensors.face.aidl;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -41,6 +43,7 @@
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.LockoutCache;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -82,6 +85,10 @@
private AuthSessionCoordinator mAuthSessionCoordinator;
@Mock
FaceProvider mFaceProvider;
+ @Mock
+ BaseClientMonitor mClientMonitor;
+ @Mock
+ AidlSession mCurrentSession;
private final TestLooper mLooper = new TestLooper();
private final LockoutCache mLockoutCache = new LockoutCache();
@@ -161,6 +168,39 @@
assertNull(sensor.getSessionForUser(USER_ID));
}
+ @Test
+ public void onBinderDied_cancelNonInterruptableClient() {
+ mLooper.dispatchAll();
+
+ when(mCurrentSession.getUserId()).thenReturn(USER_ID);
+ when(mClientMonitor.getTargetUserId()).thenReturn(USER_ID);
+ when(mClientMonitor.isInterruptable()).thenReturn(false);
+
+ final SensorProps sensorProps = new SensorProps();
+ sensorProps.commonProps = new CommonProps();
+ sensorProps.commonProps.sensorId = 1;
+ final FaceSensorPropertiesInternal internalProp = new FaceSensorPropertiesInternal(
+ sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength,
+ sensorProps.commonProps.maxEnrollmentsPerUser, null,
+ sensorProps.sensorType, sensorProps.supportsDetectInteraction,
+ sensorProps.halControlsPreview, false /* resetLockoutRequiresChallenge */);
+ final Sensor sensor = new Sensor("SensorTest", mFaceProvider, mContext, null,
+ internalProp, mLockoutResetDispatcher, mBiometricContext, mCurrentSession);
+ mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler();
+ sensor.mCurrentSession = new AidlSession(0, mock(ISession.class),
+ USER_ID, mHalCallback);
+
+ mScheduler.scheduleClientMonitor(mClientMonitor);
+
+ assertNotNull(mScheduler.getCurrentClient());
+
+ sensor.onBinderDied();
+
+ verify(mClientMonitor).cancel();
+ assertNull(sensor.getSessionForUser(USER_ID));
+ assertNull(mScheduler.getCurrentClient());
+ }
+
private void verifyNotLocked() {
assertEquals(LockoutTracker.LOCKOUT_NONE, mLockoutCache.getLockoutModeForUser(USER_ID));
verify(mLockoutResetDispatcher).notifyLockoutResetCallbacks(eq(SENSOR_ID));
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
index 0c13466..15d7601 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/SensorTest.java
@@ -17,17 +17,23 @@
package com.android.server.biometrics.sensors.fingerprint.aidl;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.common.CommonProps;
+import android.hardware.biometrics.face.SensorProps;
import android.hardware.biometrics.fingerprint.ISession;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Handler;
import android.os.test.TestLooper;
import android.platform.test.annotations.Presubmit;
@@ -37,11 +43,13 @@
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.LockoutCache;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
import com.android.server.biometrics.sensors.LockoutTracker;
import com.android.server.biometrics.sensors.UserAwareBiometricScheduler;
+import com.android.server.biometrics.sensors.fingerprint.GestureAvailabilityDispatcher;
import org.junit.Before;
import org.junit.Test;
@@ -76,6 +84,14 @@
private BiometricContext mBiometricContext;
@Mock
private AuthSessionCoordinator mAuthSessionCoordinator;
+ @Mock
+ FingerprintProvider mFingerprintProvider;
+ @Mock
+ GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
+ @Mock
+ private AidlSession mCurrentSession;
+ @Mock
+ private BaseClientMonitor mClientMonitor;
private final TestLooper mLooper = new TestLooper();
private final LockoutCache mLockoutCache = new LockoutCache();
@@ -130,6 +146,40 @@
verifyNotLocked();
}
+ @Test
+ public void onBinderDied_cancelNonInterruptableClient() {
+ mLooper.dispatchAll();
+
+ when(mCurrentSession.getUserId()).thenReturn(USER_ID);
+ when(mClientMonitor.getTargetUserId()).thenReturn(USER_ID);
+ when(mClientMonitor.isInterruptable()).thenReturn(false);
+
+ final SensorProps sensorProps = new SensorProps();
+ sensorProps.commonProps = new CommonProps();
+ sensorProps.commonProps.sensorId = 1;
+ final FingerprintSensorPropertiesInternal internalProp = new
+ FingerprintSensorPropertiesInternal(
+ sensorProps.commonProps.sensorId, sensorProps.commonProps.sensorStrength,
+ sensorProps.commonProps.maxEnrollmentsPerUser, null,
+ sensorProps.sensorType, false /* resetLockoutRequiresHardwareAuthToken */);
+ final Sensor sensor = new Sensor("SensorTest", mFingerprintProvider, mContext,
+ null /* handler */, internalProp, mLockoutResetDispatcher,
+ mGestureAvailabilityDispatcher, mBiometricContext, mCurrentSession);
+ mScheduler = (UserAwareBiometricScheduler) sensor.getScheduler();
+ sensor.mCurrentSession = new AidlSession(0, mock(ISession.class),
+ USER_ID, mHalCallback);
+
+ mScheduler.scheduleClientMonitor(mClientMonitor);
+
+ assertNotNull(mScheduler.getCurrentClient());
+
+ sensor.onBinderDied();
+
+ verify(mClientMonitor).cancel();
+ assertNull(sensor.getSessionForUser(USER_ID));
+ assertNull(mScheduler.getCurrentClient());
+ }
+
private void verifyNotLocked() {
assertEquals(LockoutTracker.LOCKOUT_NONE, mLockoutCache.getLockoutModeForUser(USER_ID));
verify(mLockoutResetDispatcher).notifyLockoutResetCallbacks(eq(SENSOR_ID));
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 0e6b412..39930bc 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -92,6 +92,7 @@
private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
private HdmiPortInfo[] mHdmiPortInfo;
private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
+ private static final int PORT_ID_EARC_SUPPORTED = 3;
@Before
public void setUp() throws Exception {
@@ -148,7 +149,7 @@
.setEarcSupported(false)
.build();
mHdmiPortInfo[2] =
- new HdmiPortInfo.Builder(3, HdmiPortInfo.PORT_INPUT, 0x2000)
+ new HdmiPortInfo.Builder(PORT_ID_EARC_SUPPORTED, HdmiPortInfo.PORT_INPUT, 0x2000)
.setCecSupported(true)
.setMhlSupported(false)
.setArcSupported(true)
@@ -1129,6 +1130,23 @@
}
@Test
+ public void disableEarc_noEarcLocalDevice_enableArc() {
+ mHdmiControlServiceSpy.clearEarcLocalDevice();
+ mHdmiControlServiceSpy.addEarcLocalDevice(
+ new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy));
+ mHdmiControlServiceSpy.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED);
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiControlServiceSpy.getEarcLocalDevice()).isNull();
+
+ Mockito.clearInvocations(mHdmiControlServiceSpy);
+ mHdmiControlServiceSpy.handleEarcStateChange(Constants.HDMI_EARC_STATUS_ARC_PENDING,
+ PORT_ID_EARC_SUPPORTED);
+ verify(mHdmiControlServiceSpy, times(1))
+ .notifyEarcStatusToAudioService(eq(false), eq(new ArrayList<>()));
+ verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(true), any());
+ }
+
+ @Test
public void disableCec_doNotClearEarcLocalDevice() {
mHdmiControlServiceSpy.clearEarcLocalDevice();
mHdmiControlServiceSpy.addEarcLocalDevice(
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
index 2b49b8a..a242cde 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java
@@ -23,6 +23,8 @@
import static com.android.internal.widget.LockPatternUtils.USER_FRP;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import android.app.PropertyInvalidatedCache;
import android.app.admin.DevicePolicyManager;
@@ -38,8 +40,9 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.nio.ByteBuffer;
-/** Test setting a lockscreen credential and then verify it under USER_FRP */
+/** Tests that involve the Factory Reset Protection (FRP) credential. */
@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
@@ -148,4 +151,68 @@
mService.verifyCredential(newPin("1234"), USER_FRP, 0 /* flags */)
.getResponseCode());
}
+
+ // The FRP block that gets written by the current version of Android must still be accepted by
+ // old versions of Android. This test tries to detect non-forward-compatible changes in
+ // PasswordData#toBytes(), which would break that.
+ @Test
+ public void testFrpBlock_isForwardsCompatible() {
+ mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
+ PersistentData data = mStorage.readPersistentDataBlock();
+ ByteBuffer buffer = ByteBuffer.wrap(data.payload);
+
+ final int credentialType = buffer.getInt();
+ assertEquals(CREDENTIAL_TYPE_PIN, credentialType);
+
+ final byte scryptLogN = buffer.get();
+ assertTrue(scryptLogN >= 0);
+
+ final byte scryptLogR = buffer.get();
+ assertTrue(scryptLogR >= 0);
+
+ final byte scryptLogP = buffer.get();
+ assertTrue(scryptLogP >= 0);
+
+ final int saltLength = buffer.getInt();
+ assertTrue(saltLength > 0);
+ final byte[] salt = new byte[saltLength];
+ buffer.get(salt);
+
+ final int passwordHandleLength = buffer.getInt();
+ assertTrue(passwordHandleLength > 0);
+ final byte[] passwordHandle = new byte[passwordHandleLength];
+ buffer.get(passwordHandle);
+ }
+
+ @Test
+ public void testFrpBlock_inBadAndroid14FormatIsAutomaticallyFixed() {
+ mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID);
+
+ // Write a "bad" FRP block with PasswordData beginning with the bytes [0, 2].
+ byte[] badPasswordData = new byte[] {
+ 0, 2, /* version 2 */
+ 0, 3, /* CREDENTIAL_TYPE_PIN */
+ 11, /* scryptLogN */
+ 22, /* scryptLogR */
+ 33, /* scryptLogP */
+ 0, 0, 0, 5, /* salt.length */
+ 1, 2, -1, -2, 55, /* salt */
+ 0, 0, 0, 6, /* passwordHandle.length */
+ 2, 3, -2, -3, 44, 1, /* passwordHandle */
+ 0, 0, 0, 6, /* pinLength */
+ };
+ mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_GATEKEEPER, PRIMARY_USER_ID, 0,
+ badPasswordData);
+
+ // Execute the code that should fix the FRP block.
+ assertFalse(mStorage.getBoolean("migrated_frp2", false, 0));
+ mService.migrateOldDataAfterSystemReady();
+ assertTrue(mStorage.getBoolean("migrated_frp2", false, 0));
+
+ // Verify that the FRP block has been fixed.
+ PersistentData data = mStorage.readPersistentDataBlock();
+ assertEquals(PersistentData.TYPE_SP_GATEKEEPER, data.type);
+ ByteBuffer buffer = ByteBuffer.wrap(data.payload);
+ assertEquals(CREDENTIAL_TYPE_PIN, buffer.getInt());
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 067feae..ce0347d 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -638,6 +638,7 @@
2, 3, -2, -3, 44, 1, /* passwordHandle */
0, 0, 0, 6, /* pinLength */
};
+ assertFalse(PasswordData.isBadFormatFromAndroid14Beta(serialized));
PasswordData deserialized = PasswordData.fromBytes(serialized);
assertEquals(11, deserialized.scryptLogN);
@@ -690,6 +691,7 @@
2, 3, -2, -3, 44, 1, /* passwordHandle */
0, 0, 0, 6, /* pinLength */
};
+ assertTrue(PasswordData.isBadFormatFromAndroid14Beta(serialized));
PasswordData deserialized = PasswordData.fromBytes(serialized);
assertEquals(11, deserialized.scryptLogN);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
index 27677e1..0e627b2 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -88,7 +88,6 @@
private static final Multimap<Class<?>, String> KNOWN_BAD =
ImmutableMultimap.<Class<?>, String>builder()
.put(Notification.Builder.class, "setPublicVersion") // b/276294099
- .putAll(RemoteViews.class, "addView", "addStableView") // b/277740082
.put(RemoteViews.class, "setIcon") // b/281018094
.put(Notification.WearableExtender.class, "addAction") // TODO: b/281044385
.put(Person.Builder.class, "setUri") // TODO: b/281044385
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/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 10f4158..0ccb0d0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -1286,7 +1286,7 @@
doReturn(bufferSize.x).when(buffer).getWidth();
doReturn(bufferSize.y).when(buffer).getHeight();
}
- return new TaskSnapshot(1, new ComponentName("", ""), buffer,
+ return new TaskSnapshot(1, 0 /* captureTime */, new ComponentName("", ""), buffer,
ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
Surface.ROTATION_0, taskSize, new Rect() /* contentInsets */,
new Rect() /* letterboxInsets*/, false /* isLowResolution */,
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
index b69874a..84c0696 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
@@ -218,7 +218,7 @@
Canvas c = buffer.lockCanvas();
c.drawColor(Color.RED);
buffer.unlockCanvasAndPost(c);
- return new TaskSnapshot(MOCK_SNAPSHOT_ID, mTopActivityComponent,
+ return new TaskSnapshot(MOCK_SNAPSHOT_ID, 0 /* captureTime */, mTopActivityComponent,
HardwareBuffer.createFromGraphicBuffer(buffer),
ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT,
mRotation, taskSize, TEST_CONTENT_INSETS, TEST_LETTERBOX_INSETS,
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);
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/TEST_MAPPING b/tests/FlickerTests/src/com/android/server/wm/flicker/TEST_MAPPING
deleted file mode 100644
index 945de33..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/TEST_MAPPING
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "ironwood-postsubmit": [
- {
- "name": "FlickerTests",
- "options": [
- {
- "include-annotation": "android.platform.test.annotations.IwTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- }
- ]
- }
- ]
-}
\ No newline at end of file