diff options
216 files changed, 6300 insertions, 5117 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index cd0bd8699794..d8ea767a6400 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -30867,6 +30867,7 @@ package android.os { field public static final int PREVIEW_SDK_INT; field public static final String RELEASE; field @NonNull public static final String RELEASE_OR_CODENAME; + field @NonNull public static final String RELEASE_OR_PREVIEW_DISPLAY; field @Deprecated public static final String SDK; field public static final int SDK_INT; field public static final String SECURITY_PATCH; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index e34498736467..083269c7c14f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -10524,6 +10524,7 @@ package android.provider { field public static final String ACTION_SHOW_ADMIN_SUPPORT_DETAILS = "android.settings.SHOW_ADMIN_SUPPORT_DETAILS"; field public static final String ACTION_TETHER_PROVISIONING_UI = "android.settings.TETHER_PROVISIONING_UI"; field public static final String ACTION_TETHER_SETTINGS = "android.settings.TETHER_SETTINGS"; + field public static final String ACTION_TETHER_UNSUPPORTED_CARRIER_UI = "android.settings.TETHER_UNSUPPORTED_CARRIER_UI"; } public static final class Settings.Global extends android.provider.Settings.NameValueTable { @@ -15701,6 +15702,11 @@ package android.util { package android.view { + @UiThread public class View implements android.view.accessibility.AccessibilityEventSource android.graphics.drawable.Drawable.Callback android.view.KeyEvent.Callback { + method @NonNull public final java.util.List<android.graphics.Rect> getUnrestrictedPreferKeepClearRects(); + method @RequiresPermission(android.Manifest.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS) public final void setUnrestrictedPreferKeepClearRects(@NonNull java.util.List<android.graphics.Rect>); + } + public abstract class Window { method public void addSystemFlags(@android.view.WindowManager.LayoutParams.SystemFlags int); } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index ed18a2219915..c36b94b424ac 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -516,6 +516,7 @@ package android.app.admin { method public boolean isCurrentInputMethodSetByOwner(); method public boolean isFactoryResetProtectionPolicySupported(); method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public boolean isNewUserDisclaimerAcknowledged(); + method public boolean isRemovingAdmin(@NonNull android.content.ComponentName, int); method @RequiresPermission(anyOf={android.Manifest.permission.MARK_DEVICE_ORGANIZATION_OWNED, android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS}, conditional=true) public void markProfileOwnerOnOrganizationOwnedDevice(@NonNull android.content.ComponentName); method @NonNull public static String operationSafetyReasonToString(int); method @NonNull public static String operationToString(int); diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index bda2e459f52a..dca5c542af17 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -165,50 +165,35 @@ public class ApplicationPackageManager extends PackageManager { public static final String PERMISSION_CONTROLLER_RESOURCE_PACKAGE = "com.android.permissioncontroller"; - private final Object mLock = new Object(); - - @GuardedBy("mLock") - private UserManager mUserManager; - @GuardedBy("mLock") - private PermissionManager mPermissionManager; - @GuardedBy("mLock") - private PackageInstaller mInstaller; - @GuardedBy("mLock") - private ArtManager mArtManager; - @GuardedBy("mLock") - private DevicePolicyManager mDevicePolicyManager; + private volatile UserManager mUserManager; + private volatile PermissionManager mPermissionManager; + private volatile PackageInstaller mInstaller; + private volatile ArtManager mArtManager; + private volatile DevicePolicyManager mDevicePolicyManager; + private volatile String mPermissionsControllerPackageName; @GuardedBy("mDelegates") private final ArrayList<MoveCallbackDelegate> mDelegates = new ArrayList<>(); - @GuardedBy("mLock") - private String mPermissionsControllerPackageName; - UserManager getUserManager() { - synchronized (mLock) { - if (mUserManager == null) { - mUserManager = UserManager.get(mContext); - } - return mUserManager; + if (mUserManager == null) { + mUserManager = UserManager.get(mContext); } + return mUserManager; } DevicePolicyManager getDevicePolicyManager() { - synchronized (mLock) { - if (mDevicePolicyManager == null) { - mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); - } - return mDevicePolicyManager; + if (mDevicePolicyManager == null) { + mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); } + return mDevicePolicyManager; } private PermissionManager getPermissionManager() { - synchronized (mLock) { - if (mPermissionManager == null) { - mPermissionManager = mContext.getSystemService(PermissionManager.class); - } - return mPermissionManager; + if (mPermissionManager == null) { + mPermissionManager = mContext.getSystemService(PermissionManager.class); } + return mPermissionManager; } @Override @@ -851,16 +836,14 @@ public class ApplicationPackageManager extends PackageManager { */ @Override public String getPermissionControllerPackageName() { - synchronized (mLock) { - if (mPermissionsControllerPackageName == null) { - try { - mPermissionsControllerPackageName = mPM.getPermissionControllerPackageName(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + if (mPermissionsControllerPackageName == null) { + try { + mPermissionsControllerPackageName = mPM.getPermissionControllerPackageName(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - return mPermissionsControllerPackageName; } + return mPermissionsControllerPackageName; } /** @@ -3235,17 +3218,15 @@ public class ApplicationPackageManager extends PackageManager { @Override public PackageInstaller getPackageInstaller() { - synchronized (mLock) { - if (mInstaller == null) { - try { - mInstaller = new PackageInstaller(mPM.getPackageInstaller(), - mContext.getPackageName(), mContext.getAttributionTag(), getUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + if (mInstaller == null) { + try { + mInstaller = new PackageInstaller(mPM.getPackageInstaller(), + mContext.getPackageName(), mContext.getAttributionTag(), getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - return mInstaller; } + return mInstaller; } @Override @@ -3583,16 +3564,14 @@ public class ApplicationPackageManager extends PackageManager { @Override public ArtManager getArtManager() { - synchronized (mLock) { - if (mArtManager == null) { - try { - mArtManager = new ArtManager(mContext, mPM.getArtManager()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + if (mArtManager == null) { + try { + mArtManager = new ArtManager(mContext, mPM.getArtManager()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } - return mArtManager; } + return mArtManager; } @Override diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index e460638d4420..c6e36a36701b 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -335,7 +335,7 @@ public class StatusBarManager { * Constant for {@link #setNavBarMode(int)} indicating kids navbar mode. * * <p>When used, back and home icons will change drawables and layout, recents will be hidden, - * and the navbar will remain visible when apps are in immersive mode. + * and enables the setting to force navbar visible, even when apps are in immersive mode. * * @hide */ diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index ddce29da8e00..58db93c123bb 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -18,6 +18,7 @@ package android.app; import android.accounts.AccountManager; import android.accounts.IAccountManager; +import android.adservices.AdServicesFrameworkInitializer; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -1565,6 +1566,7 @@ public final class SystemServiceRegistry { RoleFrameworkInitializer.registerServiceWrappers(); SchedulingFrameworkInitializer.registerServiceWrappers(); SdkSandboxManagerFrameworkInitializer.registerServiceWrappers(); + AdServicesFrameworkInitializer.registerServiceWrappers(); UwbFrameworkInitializer.registerServiceWrappers(); SafetyCenterFrameworkInitializer.registerServiceWrappers(); ConnectivityFrameworkInitializerTiramisu.registerServiceWrappers(); diff --git a/core/java/android/app/WindowConfiguration.java b/core/java/android/app/WindowConfiguration.java index b791f05c1076..5c1ab3879487 100644 --- a/core/java/android/app/WindowConfiguration.java +++ b/core/java/android/app/WindowConfiguration.java @@ -841,15 +841,6 @@ public class WindowConfiguration implements Parcelable, Comparable<WindowConfigu } /** - * Returns true if the windowingMode represents a split window. - * @hide - */ - public static boolean isSplitScreenWindowingMode(int windowingMode) { - return windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY - || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; - } - - /** * Returns true if the windows associated with this window configuration can receive input keys. * @hide */ diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index d5d14c6c0fbb..a2df2d029059 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -3766,6 +3766,7 @@ public class DevicePolicyManager { * for the user. * @hide */ + @TestApi public boolean isRemovingAdmin(@NonNull ComponentName admin, int userId) { if (mService != null) { try { diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index ebef0535f077..a03286d3ec6f 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -2674,9 +2674,8 @@ public class Resources { // Putting into a map keyed on the apk assets to deduplicate resources that are different // objects but ultimately represent the same assets Map<List<ApkAssets>, Resources> history = new ArrayMap<>(); - for (Resources r : sResourcesHistory) { - history.put(Arrays.asList(r.mResourcesImpl.mAssets.getApkAssets()), r); - } + sResourcesHistory.forEach( + r -> history.put(Arrays.asList(r.mResourcesImpl.mAssets.getApkAssets()), r)); int i = 0; for (Resources r : history.values()) { if (r != null) { diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java index ada51559a38d..3f139f02efaf 100644 --- a/core/java/android/hardware/biometrics/BiometricManager.java +++ b/core/java/android/hardware/biometrics/BiometricManager.java @@ -104,16 +104,16 @@ public class BiometricManager { public static final int BIOMETRIC_MULTI_SENSOR_DEFAULT = 0; /** - * Prefer the face sensor and fall back to fingerprint when needed. + * Use face and fingerprint sensors together. * @hide */ - public static final int BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT = 1; + public static final int BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE = 1; /** * @hide */ @IntDef({BIOMETRIC_MULTI_SENSOR_DEFAULT, - BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT}) + BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE}) @Retention(RetentionPolicy.SOURCE) public @interface BiometricMultiSensorMode {} diff --git a/core/java/android/hardware/biometrics/IBiometricService.aidl b/core/java/android/hardware/biometrics/IBiometricService.aidl index 2c3c8c353e8c..42aad36e44c1 100644 --- a/core/java/android/hardware/biometrics/IBiometricService.aidl +++ b/core/java/android/hardware/biometrics/IBiometricService.aidl @@ -63,7 +63,7 @@ interface IBiometricService { // Notify BiometricService when <Biometric>Service is ready to start the prepared client. // Client lifecycle is still managed in <Biometric>Service. - void onReadyForAuthentication(int cookie); + void onReadyForAuthentication(long requestId, int cookie); // Requests all BIOMETRIC_STRONG sensors to have their authenticatorId invalidated for the // specified user. This happens when enrollments have been added on devices with multiple diff --git a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl index 5d9b5f3bcc05..450c5ceab04c 100644 --- a/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl +++ b/core/java/android/hardware/biometrics/IBiometricSysuiReceiver.aidl @@ -30,6 +30,4 @@ oneway interface IBiometricSysuiReceiver { void onSystemEvent(int event); // Notifies that the dialog has finished animating. void onDialogAnimatedIn(); - // For multi-sensor devices, notifies that the fingerprint should start now. - void onStartFingerprintNow(); } diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java index c8b4226ecae0..07fbe4a04ff1 100644 --- a/core/java/android/os/AppZygote.java +++ b/core/java/android/os/AppZygote.java @@ -17,9 +17,11 @@ package android.os; import android.content.pm.ApplicationInfo; +import android.content.pm.ProcessInfo; import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.Zygote; import dalvik.system.VMRuntime; @@ -45,8 +47,6 @@ public class AppZygote { // Last UID/GID of the range the AppZygote can setuid()/setgid() to private final int mZygoteUidGidMax; - private final int mZygoteRuntimeFlags; - private final Object mLock = new Object(); /** @@ -57,14 +57,15 @@ public class AppZygote { private ChildZygoteProcess mZygote; private final ApplicationInfo mAppInfo; + private final ProcessInfo mProcessInfo; - public AppZygote(ApplicationInfo appInfo, int zygoteUid, int uidGidMin, int uidGidMax, - int runtimeFlags) { + public AppZygote(ApplicationInfo appInfo, ProcessInfo processInfo, int zygoteUid, int uidGidMin, + int uidGidMax) { mAppInfo = appInfo; + mProcessInfo = processInfo; mZygoteUid = zygoteUid; mZygoteUidGidMin = uidGidMin; mZygoteUidGidMax = uidGidMax; - mZygoteRuntimeFlags = runtimeFlags; } /** @@ -108,13 +109,15 @@ public class AppZygote { String abi = mAppInfo.primaryCpuAbi != null ? mAppInfo.primaryCpuAbi : Build.SUPPORTED_ABIS[0]; try { + int runtimeFlags = Zygote.getMemorySafetyRuntimeFlagsForSecondaryZygote( + mAppInfo, mProcessInfo); mZygote = Process.ZYGOTE_PROCESS.startChildZygote( "com.android.internal.os.AppZygoteInit", mAppInfo.processName + "_zygote", mZygoteUid, mZygoteUid, null, // gids - mZygoteRuntimeFlags, // runtimeFlags + runtimeFlags, "app_zygote", // seInfo abi, // abi abi, // acceptedAbiList diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 1d1f17df17df..1c85f692b232 100755 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -285,13 +285,20 @@ public class Build { public static final String RELEASE = getString("ro.build.version.release"); /** - * The version string we show to the user; may be {@link #RELEASE} or - * {@link #CODENAME} if not a final release build. + * The version string. May be {@link #RELEASE} or {@link #CODENAME} if + * not a final release build. */ @NonNull public static final String RELEASE_OR_CODENAME = getString( "ro.build.version.release_or_codename"); /** + * The version string we show to the user; may be {@link #RELEASE} or + * a descriptive string if not a final release build. + */ + @NonNull public static final String RELEASE_OR_PREVIEW_DISPLAY = getString( + "ro.build.version.release_or_preview_display"); + + /** * The base OS build the product is based on. */ public static final String BASE_OS = SystemProperties.get("ro.build.version.base_os", ""); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 8feff16c70a2..f2137b30350a 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -240,6 +240,20 @@ public final class Settings { "android.settings.TETHER_PROVISIONING_UI"; /** + * Activity Action: Show a dialog activity to notify tethering is NOT supported by carrier. + * + * When {@link android.telephony.CarrierConfigManager#KEY_CARRIER_SUPPORTS_TETHERING_BOOL} + * is false, and tethering is started by Settings, this dialog activity will be started to + * tell the user that tethering is not supported by carrier. + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + @SystemApi + public static final String ACTION_TETHER_UNSUPPORTED_CARRIER_UI = + "android.settings.TETHER_UNSUPPORTED_CARRIER_UI"; + + /** * Activity Action: Show settings to allow entering/exiting airplane mode. * <p> * In some cases, a matching Activity may not exist, so ensure you @@ -10274,6 +10288,14 @@ public final class Settings { "theme_customization_overlay_packages"; /** + * Indicates whether the nav bar is forced to always be visible, even in immersive mode. + * <p>Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String NAV_BAR_FORCE_VISIBLE = "nav_bar_force_visible"; + + /** * Indicates whether the device is in kids nav mode. * <p>Type: int (0 for false, 1 for true) * diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index 188bc3f51c52..4541f3afa428 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -95,7 +95,7 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_USE_NEW_BACKUP_ELIGIBILITY_RULES, "true"); DEFAULT_FLAGS.put(SETTINGS_ENABLE_SECURITY_HUB, "true"); DEFAULT_FLAGS.put(SETTINGS_SUPPORT_LARGE_SCREEN, "true"); - DEFAULT_FLAGS.put("settings_search_always_expand", "false"); + DEFAULT_FLAGS.put("settings_search_always_expand", "true"); DEFAULT_FLAGS.put(SETTINGS_APP_LANGUAGE_SELECTION, "true"); DEFAULT_FLAGS.put(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS, "true"); DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "false"); diff --git a/core/java/android/util/NtpTrustedTime.java b/core/java/android/util/NtpTrustedTime.java index 8604078b5ae5..40beab323576 100644 --- a/core/java/android/util/NtpTrustedTime.java +++ b/core/java/android/util/NtpTrustedTime.java @@ -265,6 +265,13 @@ public class NtpTrustedTime implements TrustedTime { return mTimeResult; } + /** Clears the last received NTP. Intended for use during tests. */ + public void clearCachedTimeResult() { + synchronized (this) { + mTimeResult = null; + } + } + private static class NtpConnectionInfo { @NonNull private final String mServer; diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index a266a28dcaca..5c7c844ae773 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -290,7 +290,8 @@ interface IWindowSession { /** * Called when the keep-clear areas for this window have changed. */ - oneway void reportKeepClearAreasChanged(IWindow window, in List<Rect> keepClearRects); + oneway void reportKeepClearAreasChanged(IWindow window, in List<Rect> restricted, + in List<Rect> unrestricted); /** * Request the server to call setInputWindowInfo on a given Surface, and return diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index b1582cf9f023..6aab6359d23e 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -98,6 +98,13 @@ public class InsetsSourceConsumer { */ private boolean mIsAnimationPending; + /** + * @param type The {@link InternalInsetsType} of the consumed insets. + * @param state The current {@link InsetsState} of the consumed insets. + * @param transactionSupplier The source of new {@link Transaction} instances. The supplier + * must provide *new* instances, which will be explicitly closed by this class. + * @param controller The {@link InsetsController} to use for insets interaction. + */ public InsetsSourceConsumer(@InternalInsetsType int type, InsetsState state, Supplier<Transaction> transactionSupplier, InsetsController controller) { mType = type; @@ -390,16 +397,17 @@ public class InsetsSourceConsumer { return; } - final Transaction t = mTransactionSupplier.get(); - if (DEBUG) Log.d(TAG, "applyRequestedVisibilityToControl: " + mRequestedVisible); - if (mRequestedVisible) { - t.show(mSourceControl.getLeash()); - } else { - t.hide(mSourceControl.getLeash()); + try (Transaction t = mTransactionSupplier.get()) { + if (DEBUG) Log.d(TAG, "applyRequestedVisibilityToControl: " + mRequestedVisible); + if (mRequestedVisible) { + t.show(mSourceControl.getLeash()); + } else { + t.hide(mSourceControl.getLeash()); + } + // Ensure the alpha value is aligned with the actual requested visibility. + t.setAlpha(mSourceControl.getLeash(), mRequestedVisible ? 1 : 0); + t.apply(); } - // Ensure the alpha value is aligned with the actual requested visibility. - t.setAlpha(mSourceControl.getLeash(), mRequestedVisible ? 1 : 0); - t.apply(); onPerceptible(mRequestedVisible); } diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 91d5b208c237..9aa243b241f7 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -133,8 +133,9 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall private boolean mDisableBackgroundLayer = false; /** - * We use this lock to protect access to mSurfaceControl. Both are accessed on the UI - * thread and the render thread via RenderNode.PositionUpdateListener#positionLost. + * We use this lock to protect access to mSurfaceControl and + * SurfaceViewPositionUpdateListener#mPositionChangedTransaction. Both are accessed on the UI + * thread and the render thread. */ final Object mSurfaceControlLock = new Object(); final Rect mTmpRect = new Rect(); @@ -223,6 +224,12 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall private final SurfaceControl.Transaction mFrameCallbackTransaction = new SurfaceControl.Transaction(); + /** + * A temporary transaction holder that should only be used when applying right away. There + * should be no assumption about thread safety for this transaction. + */ + private final SurfaceControl.Transaction mTmpTransaction = new SurfaceControl.Transaction(); + private int mParentSurfaceSequenceId; private RemoteAccessibilityController mRemoteAccessibilityController = @@ -753,7 +760,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall mBlastBufferQueue = null; } - final Transaction transaction = new Transaction(); + Transaction transaction = new Transaction(); if (mSurfaceControl != null) { transaction.remove(mSurfaceControl); mSurfaceControl = null; @@ -783,18 +790,22 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall // synchronously otherwise we may see flickers. // When the listener is updated, we will get at least a single position update call so we can // guarantee any changes we post will be applied. - private void replacePositionUpdateListener(int surfaceWidth, int surfaceHeight) { + private void replacePositionUpdateListener(int surfaceWidth, int surfaceHeight, + Transaction geometryTransaction) { if (mPositionListener != null) { mRenderNode.removePositionUpdateListener(mPositionListener); + synchronized (mSurfaceControlLock) { + geometryTransaction = mPositionListener.getTransaction().merge(geometryTransaction); + } } mPositionListener = new SurfaceViewPositionUpdateListener(surfaceWidth, surfaceHeight, - mSurfaceControl); + geometryTransaction); mRenderNode.addPositionUpdateListener(mPositionListener); } private boolean performSurfaceTransaction(ViewRootImpl viewRoot, Translator translator, boolean creating, boolean sizeChanged, boolean hintChanged, - Transaction surfaceUpdateTransaction) { + Transaction geometryTransaction) { boolean realSizeChanged = false; mSurfaceLock.lock(); @@ -809,60 +820,59 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall // SurfaceChangedCallback to update the relative z. This is needed so that // we do not change the relative z before the server is ready to swap the // parent surface. - if (creating) { - updateRelativeZ(surfaceUpdateTransaction); - if (mSurfacePackage != null) { - reparentSurfacePackage(surfaceUpdateTransaction, mSurfacePackage); - } + if (creating || (mParentSurfaceSequenceId == viewRoot.getSurfaceSequenceId())) { + updateRelativeZ(mTmpTransaction); } mParentSurfaceSequenceId = viewRoot.getSurfaceSequenceId(); if (mViewVisibility) { - surfaceUpdateTransaction.show(mSurfaceControl); + geometryTransaction.show(mSurfaceControl); } else { - surfaceUpdateTransaction.hide(mSurfaceControl); + geometryTransaction.hide(mSurfaceControl); } + if (mSurfacePackage != null) { + reparentSurfacePackage(mTmpTransaction, mSurfacePackage); + } - - updateBackgroundVisibility(surfaceUpdateTransaction); - updateBackgroundColor(surfaceUpdateTransaction); + updateBackgroundVisibility(mTmpTransaction); + updateBackgroundColor(mTmpTransaction); if (mUseAlpha) { float alpha = getFixedAlpha(); - surfaceUpdateTransaction.setAlpha(mSurfaceControl, alpha); + mTmpTransaction.setAlpha(mSurfaceControl, alpha); mSurfaceAlpha = alpha; } - surfaceUpdateTransaction.setCornerRadius(mSurfaceControl, mCornerRadius); + geometryTransaction.setCornerRadius(mSurfaceControl, mCornerRadius); if ((sizeChanged || hintChanged) && !creating) { - setBufferSize(surfaceUpdateTransaction); + setBufferSize(geometryTransaction); } if (sizeChanged || creating || !isHardwareAccelerated()) { + onSetSurfacePositionAndScaleRT(geometryTransaction, mSurfaceControl, + mScreenRect.left, /*positionLeft*/ + mScreenRect.top /*positionTop*/ , + mScreenRect.width() / (float) mSurfaceWidth /*postScaleX*/, + mScreenRect.height() / (float) mSurfaceHeight /*postScaleY*/); // Set a window crop when creating the surface or changing its size to // crop the buffer to the surface size since the buffer producer may // use SCALING_MODE_SCALE and submit a larger size than the surface // size. if (mClipSurfaceToBounds && mClipBounds != null) { - surfaceUpdateTransaction.setWindowCrop(mSurfaceControl, mClipBounds); + geometryTransaction.setWindowCrop(mSurfaceControl, mClipBounds); } else { - surfaceUpdateTransaction.setWindowCrop(mSurfaceControl, mSurfaceWidth, + geometryTransaction.setWindowCrop(mSurfaceControl, mSurfaceWidth, mSurfaceHeight); } - surfaceUpdateTransaction.setDesintationFrame(mBlastSurfaceControl, mSurfaceWidth, + geometryTransaction.setDesintationFrame(mBlastSurfaceControl, mSurfaceWidth, mSurfaceHeight); if (isHardwareAccelerated()) { // This will consume the passed in transaction and the transaction will be // applied on a render worker thread. - replacePositionUpdateListener(mSurfaceWidth, mSurfaceHeight); - } else { - onSetSurfacePositionAndScale(surfaceUpdateTransaction, mSurfaceControl, - mScreenRect.left /*positionLeft*/, - mScreenRect.top /*positionTop*/, - mScreenRect.width() / (float) mSurfaceWidth /*postScaleX*/, - mScreenRect.height() / (float) mSurfaceHeight /*postScaleY*/); + replacePositionUpdateListener(mSurfaceWidth, mSurfaceHeight, + geometryTransaction); } if (DEBUG_POSITION) { Log.d(TAG, String.format( @@ -874,7 +884,8 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall mScreenRect.bottom, mSurfaceWidth, mSurfaceHeight)); } } - applyTransactionOnVriDraw(surfaceUpdateTransaction); + mTmpTransaction.merge(geometryTransaction); + mTmpTransaction.apply(); updateEmbeddedAccessibilityMatrix(); mSurfaceFrame.left = 0; @@ -982,17 +993,17 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall mScreenRect.offset(surfaceInsets.left, surfaceInsets.top); // Collect all geometry changes and apply these changes on the RenderThread worker // via the RenderNode.PositionUpdateListener. - final Transaction surfaceUpdateTransaction = new Transaction(); + final Transaction geometryTransaction = new Transaction(); if (creating) { updateOpaqueFlag(); final String name = "SurfaceView[" + viewRoot.getTitle().toString() + "]"; - createBlastSurfaceControls(viewRoot, name, surfaceUpdateTransaction); + createBlastSurfaceControls(viewRoot, name, geometryTransaction); } else if (mSurfaceControl == null) { return; } final boolean realSizeChanged = performSurfaceTransaction(viewRoot, - translator, creating, sizeChanged, hintChanged, surfaceUpdateTransaction); + translator, creating, sizeChanged, hintChanged, geometryTransaction); final boolean redrawNeeded = sizeChanged || creating || hintChanged || (mVisible && !mDrawFinished); @@ -1128,7 +1139,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall * */ private void createBlastSurfaceControls(ViewRootImpl viewRoot, String name, - Transaction surfaceUpdateTransaction) { + Transaction geometryTransaction) { if (mSurfaceControl == null) { mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession) .setName(name) @@ -1151,10 +1162,11 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall .build(); } else { // update blast layer - surfaceUpdateTransaction + mTmpTransaction .setOpaque(mBlastSurfaceControl, (mSurfaceFlags & SurfaceControl.OPAQUE) != 0) .setSecure(mBlastSurfaceControl, (mSurfaceFlags & SurfaceControl.SECURE) != 0) - .show(mBlastSurfaceControl); + .show(mBlastSurfaceControl) + .apply(); } if (mBackgroundControl == null) { @@ -1201,7 +1213,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall * * @hide */ - protected void onSetSurfacePositionAndScale(@NonNull Transaction transaction, + protected void onSetSurfacePositionAndScaleRT(@NonNull Transaction transaction, @NonNull SurfaceControl surface, int positionLeft, int positionTop, float postScaleX, float postScaleY) { transaction.setPosition(surface, positionLeft, positionTop); @@ -1214,14 +1226,12 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall if (mSurfaceControl == null) { return; } - final Transaction transaction = new Transaction(); - onSetSurfacePositionAndScale(transaction, mSurfaceControl, + onSetSurfacePositionAndScaleRT(mTmpTransaction, mSurfaceControl, mScreenRect.left, /*positionLeft*/ mScreenRect.top/*positionTop*/ , mScreenRect.width() / (float) mSurfaceWidth /*postScaleX*/, mScreenRect.height() / (float) mSurfaceHeight /*postScaleY*/); - applyTransactionOnVriDraw(transaction); - invalidate(); + mTmpTransaction.apply(); } /** @@ -1243,57 +1253,66 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall } } - private final Rect mRTLastReportedPosition = new Rect(); - private final Point mRTLastReportedSurfaceSize = new Point(); + private Rect mRTLastReportedPosition = new Rect(); + private Point mRTLastReportedSurfaceSize = new Point(); private class SurfaceViewPositionUpdateListener implements RenderNode.PositionUpdateListener { - private final int mRtSurfaceWidth; - private final int mRtSurfaceHeight; + int mRtSurfaceWidth = -1; + int mRtSurfaceHeight = -1; private final SurfaceControl.Transaction mPositionChangedTransaction = new SurfaceControl.Transaction(); - private final SurfaceControl mRtSurfaceControl = new SurfaceControl(); + boolean mPendingTransaction = false; SurfaceViewPositionUpdateListener(int surfaceWidth, int surfaceHeight, - SurfaceControl surfaceControl) { + @Nullable Transaction t) { mRtSurfaceWidth = surfaceWidth; mRtSurfaceHeight = surfaceHeight; - mRtSurfaceControl.copyFrom(surfaceControl, "SurfaceViewPositionUpdateListener"); + if (t != null) { + mPositionChangedTransaction.merge(t); + mPendingTransaction = true; + } } @Override public void positionChanged(long frameNumber, int left, int top, int right, int bottom) { - if (mRTLastReportedPosition.left == left - && mRTLastReportedPosition.top == top - && mRTLastReportedPosition.right == right - && mRTLastReportedPosition.bottom == bottom - && mRTLastReportedSurfaceSize.x == mRtSurfaceWidth - && mRTLastReportedSurfaceSize.y == mRtSurfaceHeight) { - return; - } - try { - if (DEBUG_POSITION) { - Log.d(TAG, String.format( - "%d updateSurfacePosition RenderWorker, frameNr = %d, " - + "position = [%d, %d, %d, %d] surfaceSize = %dx%d", - System.identityHashCode(SurfaceView.this), frameNumber, - left, top, right, bottom, mRtSurfaceWidth, mRtSurfaceHeight)); + synchronized(mSurfaceControlLock) { + if (mSurfaceControl == null) { + return; } - mRTLastReportedPosition.set(left, top, right, bottom); - mRTLastReportedSurfaceSize.set(mRtSurfaceWidth, mRtSurfaceHeight); - onSetSurfacePositionAndScale(mPositionChangedTransaction, mRtSurfaceControl, - mRTLastReportedPosition.left /*positionLeft*/, - mRTLastReportedPosition.top /*positionTop*/, - mRTLastReportedPosition.width() - / (float) mRtSurfaceWidth /*postScaleX*/, - mRTLastReportedPosition.height() - / (float) mRtSurfaceHeight /*postScaleY*/); - if (mViewVisibility) { - // b/131239825 - mPositionChangedTransaction.show(mRtSurfaceControl); + if (mRTLastReportedPosition.left == left + && mRTLastReportedPosition.top == top + && mRTLastReportedPosition.right == right + && mRTLastReportedPosition.bottom == bottom + && mRTLastReportedSurfaceSize.x == mRtSurfaceWidth + && mRTLastReportedSurfaceSize.y == mRtSurfaceHeight + && !mPendingTransaction) { + return; + } + try { + if (DEBUG_POSITION) { + Log.d(TAG, String.format( + "%d updateSurfacePosition RenderWorker, frameNr = %d, " + + "position = [%d, %d, %d, %d] surfaceSize = %dx%d", + System.identityHashCode(SurfaceView.this), frameNumber, + left, top, right, bottom, mRtSurfaceWidth, mRtSurfaceHeight)); + } + mRTLastReportedPosition.set(left, top, right, bottom); + mRTLastReportedSurfaceSize.set(mRtSurfaceWidth, mRtSurfaceHeight); + onSetSurfacePositionAndScaleRT(mPositionChangedTransaction, mSurfaceControl, + mRTLastReportedPosition.left /*positionLeft*/, + mRTLastReportedPosition.top /*positionTop*/, + mRTLastReportedPosition.width() + / (float) mRtSurfaceWidth /*postScaleX*/, + mRTLastReportedPosition.height() + / (float) mRtSurfaceHeight /*postScaleY*/); + if (mViewVisibility) { + mPositionChangedTransaction.show(mSurfaceControl); + } + applyOrMergeTransaction(mPositionChangedTransaction, frameNumber); + mPendingTransaction = false; + } catch (Exception ex) { + Log.e(TAG, "Exception from repositionChild", ex); } - applyOrMergeTransaction(mPositionChangedTransaction, frameNumber); - } catch (Exception ex) { - Log.e(TAG, "Exception from repositionChild", ex); } } @@ -1302,7 +1321,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall float vecX, float vecY, float maxStretchX, float maxStretchY, float childRelativeLeft, float childRelativeTop, float childRelativeRight, float childRelativeBottom) { - mRtTransaction.setStretchEffect(mRtSurfaceControl, width, height, vecX, vecY, + mRtTransaction.setStretchEffect(mSurfaceControl, width, height, vecX, vecY, maxStretchX, maxStretchY, childRelativeLeft, childRelativeTop, childRelativeRight, childRelativeBottom); applyOrMergeTransaction(mRtTransaction, frameNumber); @@ -1317,14 +1336,28 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall mRTLastReportedPosition.setEmpty(); mRTLastReportedSurfaceSize.set(-1, -1); - // positionLost can be called while UI thread is un-paused. + /** + * positionLost can be called while UI thread is un-paused so we + * need to hold the lock here. + */ synchronized (mSurfaceControlLock) { - if (mSurfaceControl == null) return; - // b/131239825 + if (mPendingTransaction) { + Log.w(TAG, System.identityHashCode(SurfaceView.this) + + "Pending transaction cleared."); + mPositionChangedTransaction.clear(); + mPendingTransaction = false; + } + if (mSurfaceControl == null) { + return; + } mRtTransaction.hide(mSurfaceControl); applyOrMergeTransaction(mRtTransaction, frameNumber); } } + + public Transaction getTransaction() { + return mPositionChangedTransaction; + } } private SurfaceViewPositionUpdateListener mPositionListener = null; @@ -1371,10 +1404,8 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall * @hide */ public void setResizeBackgroundColor(int bgColor) { - final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); - setResizeBackgroundColor(transaction, bgColor); - applyTransactionOnVriDraw(transaction); - invalidate(); + setResizeBackgroundColor(mTmpTransaction, bgColor); + mTmpTransaction.apply(); } /** diff --git a/core/java/android/view/SyncRtSurfaceTransactionApplier.java b/core/java/android/view/SyncRtSurfaceTransactionApplier.java index 3e2110341693..e9c937cc0f9b 100644 --- a/core/java/android/view/SyncRtSurfaceTransactionApplier.java +++ b/core/java/android/view/SyncRtSurfaceTransactionApplier.java @@ -65,10 +65,12 @@ public class SyncRtSurfaceTransactionApplier { applyParams(t, params); mTargetViewRootImpl.registerRtFrameCallback(frame -> { - if (mTargetSc == null || !mTargetSc.isValid()) { - return; + if (mTargetSc != null && mTargetSc.isValid()) { + applyTransaction(t, frame); } - applyTransaction(t, frame); + // The transaction was either dropped, successfully applied, or merged with a future + // transaction, so we can safely release its resources. + t.close(); }); // Make sure a frame gets scheduled. diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 553c537d9b95..8b3a29a7aa6f 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -46,9 +46,11 @@ import android.annotation.IntRange; import android.annotation.LayoutRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.Size; import android.annotation.StyleRes; import android.annotation.SuppressLint; +import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UiContext; import android.annotation.UiThread; @@ -4744,6 +4746,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private List<Rect> mSystemGestureExclusionRects = null; private List<Rect> mKeepClearRects = null; + private List<Rect> mUnrestrictedKeepClearRects = null; private boolean mPreferKeepClear = false; private Rect mHandwritingArea = null; @@ -11729,6 +11732,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, final ListenerInfo info = getListenerInfo(); if (getSystemGestureExclusionRects().isEmpty() && collectPreferKeepClearRects().isEmpty() + && collectUnrestrictedPreferKeepClearRects().isEmpty() && (info.mHandwritingArea == null || !isAutoHandwritingEnabled())) { if (info.mPositionUpdateListener != null) { mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener); @@ -11871,6 +11875,52 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return Collections.emptyList(); } + /** + * Set a preference to keep the provided rects clear from floating windows above this + * view's window. This informs the system that these rects are considered vital areas for the + * user and that ideally they should not be covered. Setting this is only appropriate for UI + * where the user would likely take action to uncover it. + * <p> + * Note: The difference with {@link #setPreferKeepClearRects} is that the system won't apply + * restrictions to the rects set here. + * <p> + * @see #setPreferKeepClear + * @see #getPreferKeepClearRects + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS) + public final void setUnrestrictedPreferKeepClearRects(@NonNull List<Rect> rects) { + final ListenerInfo info = getListenerInfo(); + if (info.mUnrestrictedKeepClearRects != null) { + info.mUnrestrictedKeepClearRects.clear(); + info.mUnrestrictedKeepClearRects.addAll(rects); + } else { + info.mUnrestrictedKeepClearRects = new ArrayList<>(rects); + } + updatePositionUpdateListener(); + postUpdate(this::updateKeepClearRects); + } + + /** + * @return the list of rects, set by {@link #setPreferKeepClearRects}. + * + * @see #setPreferKeepClearRects + * + * @hide + */ + @SystemApi + @NonNull + public final List<Rect> getUnrestrictedPreferKeepClearRects() { + final ListenerInfo info = mListenerInfo; + if (info != null && info.mKeepClearRects != null) { + return new ArrayList(info.mUnrestrictedKeepClearRects); + } + + return Collections.emptyList(); + } + void updateKeepClearRects() { final AttachInfo ai = mAttachInfo; if (ai != null) { @@ -11899,6 +11949,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Retrieve the list of unrestricted areas within this view's post-layout coordinate space + * which the system will try to not cover with other floating elements, like the pip window. + */ + @NonNull + List<Rect> collectUnrestrictedPreferKeepClearRects() { + final ListenerInfo info = mListenerInfo; + if (info != null && info.mUnrestrictedKeepClearRects != null) { + return info.mUnrestrictedKeepClearRects; + } + + return Collections.emptyList(); + } + + /** * Set a list of handwriting areas in this view. If there is any stylus {@link MotionEvent} * occurs within those areas, it will trigger stylus handwriting mode. This can be disabled by * disabling the auto handwriting initiation by calling diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index a9a2689bd318..d3d36255d05a 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -221,6 +221,7 @@ import java.io.StringWriter; import java.lang.ref.WeakReference; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -778,6 +779,8 @@ public final class ViewRootImpl implements ViewParent, new ViewRootRectTracker(v -> v.getSystemGestureExclusionRects()); private final ViewRootRectTracker mKeepClearRectsTracker = new ViewRootRectTracker(v -> v.collectPreferKeepClearRects()); + private final ViewRootRectTracker mUnrestrictedKeepClearRectsTracker = + new ViewRootRectTracker(v -> v.collectUnrestrictedPreferKeepClearRects()); private IAccessibilityEmbeddedConnection mAccessibilityEmbeddedConnection; @@ -4178,7 +4181,7 @@ public final class ViewRootImpl implements ViewParent, + " didProduceBuffer=" + didProduceBuffer); } - Transaction tmpTransaction = new Transaction(); + final Transaction tmpTransaction = new Transaction(); tmpTransaction.merge(mRtBLASTSyncTransaction); // If frame wasn't drawn, clear out the next transaction so it doesn't affect the next @@ -4209,6 +4212,7 @@ public final class ViewRootImpl implements ViewParent, blastSyncConsumer.accept(mSurfaceChangedTransaction); } } + tmpTransaction.close(); if (reportNextDraw) { pendingDrawFinished(); @@ -4878,14 +4882,26 @@ public final class ViewRootImpl implements ViewParent, */ void updateKeepClearRectsForView(View view) { mKeepClearRectsTracker.updateRectsForView(view); + mUnrestrictedKeepClearRectsTracker.updateRectsForView(view); mHandler.sendEmptyMessage(MSG_KEEP_CLEAR_RECTS_CHANGED); } void keepClearRectsChanged() { - final List<Rect> rectsForWindowManager = mKeepClearRectsTracker.computeChangedRects(); - if (rectsForWindowManager != null && mView != null) { + List<Rect> restrictedKeepClearRects = mKeepClearRectsTracker.computeChangedRects(); + List<Rect> unrestrictedKeepClearRects = + mUnrestrictedKeepClearRectsTracker.computeChangedRects(); + if ((restrictedKeepClearRects != null || unrestrictedKeepClearRects != null) + && mView != null) { + if (restrictedKeepClearRects == null) { + restrictedKeepClearRects = Collections.emptyList(); + } + if (unrestrictedKeepClearRects == null) { + unrestrictedKeepClearRects = Collections.emptyList(); + } + try { - mWindowSession.reportKeepClearAreasChanged(mWindow, rectsForWindowManager); + mWindowSession.reportKeepClearAreasChanged(mWindow, restrictedKeepClearRects, + unrestrictedKeepClearRects); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index c81b8ccf94d6..a270c9283ddc 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -31,6 +31,7 @@ import android.window.ClientWindowFrames; import android.window.IOnBackInvokedCallback; import java.util.HashMap; +import java.util.List; import java.util.Objects; /** @@ -473,12 +474,12 @@ public class WindowlessWindowManager implements IWindowSession { @Override public void reportSystemGestureExclusionChanged(android.view.IWindow window, - java.util.List<android.graphics.Rect> exclusionRects) { + List<Rect> exclusionRects) { } @Override - public void reportKeepClearAreasChanged(android.view.IWindow window, - java.util.List<android.graphics.Rect> exclusionRects) { + public void reportKeepClearAreasChanged(android.view.IWindow window, List<Rect> restrictedRects, + List<Rect> unrestrictedRects) { } @Override diff --git a/core/java/android/view/accessibility/IWindowMagnificationConnection.aidl b/core/java/android/view/accessibility/IWindowMagnificationConnection.aidl index 67d96678f420..62d029bd1be6 100644 --- a/core/java/android/view/accessibility/IWindowMagnificationConnection.aidl +++ b/core/java/android/view/accessibility/IWindowMagnificationConnection.aidl @@ -32,7 +32,7 @@ oneway interface IWindowMagnificationConnection { /** * Enables window magnification on specified display with given center and scale and animation. * - * @param displayId The logical display id. + * @param displayId the logical display id. * @param scale magnification scale. * @param centerX the screen-relative X coordinate around which to center, * or {@link Float#NaN} to leave unchanged. @@ -51,7 +51,7 @@ oneway interface IWindowMagnificationConnection { /** * Sets the scale of the window magnifier on specified display. * - * @param displayId The logical display id. + * @param displayId the logical display id. * @param scale magnification scale. */ void setScale(int displayId, float scale); @@ -59,7 +59,7 @@ oneway interface IWindowMagnificationConnection { /** * Disables window magnification on specified display with animation. * - * @param displayId The logical display id. + * @param displayId the logical display id. * @param callback The callback called when the animation is completed or interrupted. */ void disableWindowMagnification(int displayId, @@ -68,6 +68,7 @@ oneway interface IWindowMagnificationConnection { /** * Moves the window magnifier on the specified display. It has no effect while animating. * + * @param displayId the logical display id. * @param offsetX the amount in pixels to offset the window magnifier in the X direction, in * current screen pixels. * @param offsetY the amount in pixels to offset the window magnifier in the Y direction, in @@ -76,9 +77,20 @@ oneway interface IWindowMagnificationConnection { void moveWindowMagnifier(int displayId, float offsetX, float offsetY); /** + * Moves the window magnifier on the given display. + * + * @param displayId the logical display id. + * @param positionX the x-axis position of the center of the magnified source bounds. + * @param positionY the y-axis position of the center of the magnified source bounds. + * @param callback the callback called when the animation is completed or interrupted. + */ + void moveWindowMagnifierToPosition(int displayId, float positionX, float positionY, + in IRemoteMagnificationAnimationCallback callback); + + /** * Requests System UI show magnification mode button UI on the specified display. * - * @param displayId The logical display id. + * @param displayId the logical display id. * @param magnificationMode the current magnification mode. */ void showMagnificationButton(int displayId, int magnificationMode); @@ -86,7 +98,7 @@ oneway interface IWindowMagnificationConnection { /** * Requests System UI remove magnification mode button UI on the specified display. * - * @param displayId The logical display id. + * @param displayId the logical display id. */ void removeMagnificationButton(int displayId); diff --git a/core/java/android/view/accessibility/IWindowMagnificationConnectionCallback.aidl b/core/java/android/view/accessibility/IWindowMagnificationConnectionCallback.aidl index 722546eb06e4..adfeb6d11008 100644 --- a/core/java/android/view/accessibility/IWindowMagnificationConnectionCallback.aidl +++ b/core/java/android/view/accessibility/IWindowMagnificationConnectionCallback.aidl @@ -68,12 +68,10 @@ import android.graphics.Rect; void onAccessibilityActionPerformed(int displayId); /** - * Called when the user is performing dragging gesture. It is started after the offset - * between the down location and the move event location exceed - * {@link ViewConfiguration#getScaledTouchSlop()}. + * Called when the user is performing move action. * * @param displayId The logical display id. */ - void onDrag(int displayId); + void onMove(int displayId); } diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java index 2bfbe4bdfba7..bc7a5fda6f7a 100644 --- a/core/java/android/webkit/WebViewZygote.java +++ b/core/java/android/webkit/WebViewZygote.java @@ -25,6 +25,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.os.Zygote; /** @hide */ public class WebViewZygote { @@ -127,13 +128,15 @@ public class WebViewZygote { try { String abi = sPackage.applicationInfo.primaryCpuAbi; + int runtimeFlags = Zygote.getMemorySafetyRuntimeFlagsForSecondaryZygote( + sPackage.applicationInfo, null); sZygote = Process.ZYGOTE_PROCESS.startChildZygote( "com.android.internal.os.WebViewZygoteInit", "webview_zygote", Process.WEBVIEW_ZYGOTE_UID, Process.WEBVIEW_ZYGOTE_UID, null, // gids - 0, // runtimeFlags + runtimeFlags, "webview_zygote", // seInfo abi, // abi TextUtils.join(",", Build.SUPPORTED_ABIS), diff --git a/core/java/android/widget/inline/InlineContentView.java b/core/java/android/widget/inline/InlineContentView.java index e4f483a29343..9712311aab7c 100644 --- a/core/java/android/widget/inline/InlineContentView.java +++ b/core/java/android/widget/inline/InlineContentView.java @@ -230,9 +230,8 @@ public class InlineContentView extends ViewGroup { int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mSurfaceView = new SurfaceView(context, attrs, defStyleAttr, defStyleRes) { - // b/219807628 @Override - protected void onSetSurfacePositionAndScale( + protected void onSetSurfacePositionAndScaleRT( @NonNull SurfaceControl.Transaction transaction, @NonNull SurfaceControl surface, int positionLeft, int positionTop, float postScaleX, float postScaleY) { @@ -249,7 +248,7 @@ public class InlineContentView extends ViewGroup { postScaleX = InlineContentView.this.getScaleX(); postScaleY = InlineContentView.this.getScaleY(); - super.onSetSurfacePositionAndScale(transaction, surface, positionLeft, + super.onSetSurfacePositionAndScaleRT(transaction, surface, positionLeft, positionTop, postScaleX, postScaleY); } }; diff --git a/core/java/android/window/SplashScreenView.java b/core/java/android/window/SplashScreenView.java index 34a34180b227..232248b6fab3 100644 --- a/core/java/android/window/SplashScreenView.java +++ b/core/java/android/window/SplashScreenView.java @@ -65,6 +65,7 @@ import com.android.internal.util.ContrastColorUtil; import java.time.Duration; import java.time.Instant; import java.util.function.Consumer; +import java.util.function.LongConsumer; /** * <p>The view which allows an activity to customize its splash screen exit animation.</p> @@ -234,7 +235,7 @@ public final class SplashScreenView extends FrameLayout { /** * Set the animation duration if icon is animatable. */ - public Builder setAnimationDurationMillis(int duration) { + public Builder setAnimationDurationMillis(long duration) { mIconAnimationDuration = Duration.ofMillis(duration); return this; } @@ -521,8 +522,11 @@ public final class SplashScreenView extends FrameLayout { }); } - private void animationStartCallback() { + private void animationStartCallback(long animDuration) { mIconAnimationStart = Instant.now(); + if (animDuration > 0) { + mIconAnimationDuration = Duration.ofMillis(animDuration); + } } /** @@ -693,9 +697,8 @@ public final class SplashScreenView extends FrameLayout { * Prepare the animation if this drawable also be animatable. * @param duration The animation duration. * @param startListener The callback listener used to receive the start of the animation. - * @return true if this drawable object can also be animated and it can be played now. */ - boolean prepareAnimate(long duration, Runnable startListener); + void prepareAnimate(long duration, LongConsumer startListener); /** * Stop animation. diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 94e5ea960f4f..909e2770ddcc 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -54,7 +54,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { private static final String TAG = "WindowOnBackDispatcher"; private static final String BACK_PREDICTABILITY_PROP = "persist.debug.back_predictability"; private static final boolean IS_BACK_PREDICTABILITY_ENABLED = SystemProperties - .getInt(BACK_PREDICTABILITY_PROP, 1) > 0; + .getInt(BACK_PREDICTABILITY_PROP, 0) > 0; /** Convenience hashmap to quickly decide if a callback has been added. */ private final HashMap<OnBackInvokedCallback, Integer> mAllCallbacks = new HashMap<>(); diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 4ae6bf7e8379..70506ccaebb8 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -771,11 +771,6 @@ public class ChooserActivity extends ResolverActivity implements delegationIntent.setComponent(delegateActivity); delegationIntent.putExtra(Intent.EXTRA_INTENT, getIntent()); delegationIntent.putExtra(ActivityTaskManager.EXTRA_PERMISSION_TOKEN, permissionToken); - - // Query prediction availability; mIsAppPredictorComponentAvailable isn't initialized. - delegationIntent.putExtra( - EXTRA_IS_APP_PREDICTION_SERVICE_AVAILABLE, isAppPredictionServiceAvailable()); - delegationIntent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); // Don't close until the delegate finishes, or the token will be invalidated. @@ -972,34 +967,8 @@ public class ChooserActivity extends ResolverActivity implements /** * Returns true if app prediction service is defined and the component exists on device. */ - @VisibleForTesting - public boolean isAppPredictionServiceAvailable() { - if (getPackageManager().getAppPredictionServicePackageName() == null) { - // Default AppPredictionService is not defined. - return false; - } - - final String appPredictionServiceName = - getString(R.string.config_defaultAppPredictionService); - if (appPredictionServiceName == null) { - return false; - } - final ComponentName appPredictionComponentName = - ComponentName.unflattenFromString(appPredictionServiceName); - if (appPredictionComponentName == null) { - return false; - } - - // Check if the app prediction component actually exists on the device. The component is - // only visible when this is running in a system activity; otherwise this check will fail. - Intent intent = new Intent(); - intent.setComponent(appPredictionComponentName); - if (getPackageManager().resolveService(intent, PackageManager.MATCH_ALL) == null) { - Log.e(TAG, "App prediction service is defined, but does not exist: " - + appPredictionServiceName); - return false; - } - return true; + private boolean isAppPredictionServiceAvailable() { + return getPackageManager().getAppPredictionServicePackageName() != null; } /** diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index 6d4b8c5ea1ad..b1e7d15cbf4a 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -20,13 +20,21 @@ import static android.system.OsConstants.O_CLOEXEC; import android.annotation.NonNull; import android.annotation.Nullable; +import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; +import android.compat.annotation.EnabledAfter; +import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.ProcessInfo; import android.net.Credentials; import android.net.LocalServerSocket; import android.net.LocalSocket; +import android.os.Build; import android.os.FactoryTest; import android.os.IVold; import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; import android.os.SystemProperties; import android.os.Trace; import android.provider.DeviceConfig; @@ -34,6 +42,7 @@ import android.system.ErrnoException; import android.system.Os; import android.util.Log; +import com.android.internal.compat.IPlatformCompat; import com.android.internal.net.NetworkUtilsInternal; import dalvik.annotation.optimization.CriticalNative; @@ -125,6 +134,7 @@ public final class Zygote { public static final int MEMORY_TAG_LEVEL_MASK = (1 << 19) | (1 << 20); public static final int MEMORY_TAG_LEVEL_NONE = 0; + /** * Enable pointer tagging in this process. * Tags are checked during memory deallocation, but not on access. @@ -170,10 +180,8 @@ public final class Zygote { */ public static final int GWP_ASAN_LEVEL_ALWAYS = 1 << 22; - /** - * Enable automatic zero-initialization of native heap memory allocations. - */ - public static final int NATIVE_HEAP_ZERO_INIT = 1 << 23; + /** Enable automatic zero-initialization of native heap memory allocations. */ + public static final int NATIVE_HEAP_ZERO_INIT_ENABLED = 1 << 23; /** * Enable profiling from system services. This loads profiling related plugins in ART. @@ -1170,4 +1178,251 @@ public final class Zygote { * we failed to determine the level. */ public static native int nativeCurrentTaggingLevel(); + + /** + * Native heap allocations will now have a non-zero tag in the most significant byte. + * + * @see <a href="https://source.android.com/devices/tech/debug/tagged-pointers">Tagged + * Pointers</a> + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q) + private static final long NATIVE_HEAP_POINTER_TAGGING = 135754954; // This is a bug id. + + /** + * Native heap allocations in AppZygote process and its descendants will now have a non-zero tag + * in the most significant byte. + * + * @see <a href="https://source.android.com/devices/tech/debug/tagged-pointers">Tagged + * Pointers</a> + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S) + private static final long NATIVE_HEAP_POINTER_TAGGING_SECONDARY_ZYGOTE = 207557677; + + /** + * Enable asynchronous (ASYNC) memory tag checking in this process. This flag will only have an + * effect on hardware supporting the ARM Memory Tagging Extension (MTE). + */ + @ChangeId @Disabled + private static final long NATIVE_MEMTAG_ASYNC = 135772972; // This is a bug id. + + /** + * Enable synchronous (SYNC) memory tag checking in this process. This flag will only have an + * effect on hardware supporting the ARM Memory Tagging Extension (MTE). If both + * NATIVE_MEMTAG_ASYNC and this option is selected, this option takes preference and MTE is + * enabled in SYNC mode. + */ + @ChangeId @Disabled + private static final long NATIVE_MEMTAG_SYNC = 177438394; // This is a bug id. + + /** Enable automatic zero-initialization of native heap memory allocations. */ + @ChangeId @Disabled + private static final long NATIVE_HEAP_ZERO_INIT = 178038272; // This is a bug id. + + /** + * Enable sampled memory bug detection in the app. + * + * @see <a href="https://source.android.com/devices/tech/debug/gwp-asan">GWP-ASan</a>. + */ + @ChangeId @Disabled private static final long GWP_ASAN = 135634846; // This is a bug id. + + private static int memtagModeToZygoteMemtagLevel(int memtagMode) { + switch (memtagMode) { + case ApplicationInfo.MEMTAG_ASYNC: + return MEMORY_TAG_LEVEL_ASYNC; + case ApplicationInfo.MEMTAG_SYNC: + return MEMORY_TAG_LEVEL_SYNC; + default: + return MEMORY_TAG_LEVEL_NONE; + } + } + + private static boolean isCompatChangeEnabled( + long change, + @NonNull ApplicationInfo info, + @Nullable IPlatformCompat platformCompat, + int enabledAfter) { + try { + if (platformCompat != null) return platformCompat.isChangeEnabled(change, info); + } catch (RemoteException ignore) { + } + return enabledAfter > 0 && info.targetSdkVersion > enabledAfter; + } + + // Returns the requested memory tagging level. + private static int getRequestedMemtagLevel( + @NonNull ApplicationInfo info, + @Nullable ProcessInfo processInfo, + @Nullable IPlatformCompat platformCompat) { + // Look at the process attribute first. + if (processInfo != null && processInfo.memtagMode != ApplicationInfo.MEMTAG_DEFAULT) { + return memtagModeToZygoteMemtagLevel(processInfo.memtagMode); + } + + // Then at the application attribute. + if (info.getMemtagMode() != ApplicationInfo.MEMTAG_DEFAULT) { + return memtagModeToZygoteMemtagLevel(info.getMemtagMode()); + } + + if (isCompatChangeEnabled(NATIVE_MEMTAG_SYNC, info, platformCompat, 0)) { + return MEMORY_TAG_LEVEL_SYNC; + } + + if (isCompatChangeEnabled(NATIVE_MEMTAG_ASYNC, info, platformCompat, 0)) { + return MEMORY_TAG_LEVEL_ASYNC; + } + + // Check to ensure the app hasn't explicitly opted-out of TBI via. the manifest attribute. + if (!info.allowsNativeHeapPointerTagging()) { + return MEMORY_TAG_LEVEL_NONE; + } + + String defaultLevel = SystemProperties.get("persist.arm64.memtag.app_default"); + if ("sync".equals(defaultLevel)) { + return MEMORY_TAG_LEVEL_SYNC; + } else if ("async".equals(defaultLevel)) { + return MEMORY_TAG_LEVEL_ASYNC; + } + + // Check to see that the compat feature for TBI is enabled. + if (isCompatChangeEnabled( + NATIVE_HEAP_POINTER_TAGGING, info, platformCompat, Build.VERSION_CODES.Q)) { + return MEMORY_TAG_LEVEL_TBI; + } + + return MEMORY_TAG_LEVEL_NONE; + } + + private static int decideTaggingLevel( + @NonNull ApplicationInfo info, + @Nullable ProcessInfo processInfo, + @Nullable IPlatformCompat platformCompat) { + // Get the desired tagging level (app manifest + compat features). + int level = getRequestedMemtagLevel(info, processInfo, platformCompat); + + // Take into account the hardware capabilities. + if (nativeSupportsMemoryTagging()) { + // MTE devices can not do TBI, because the Zygote process already has live MTE + // allocations. Downgrade TBI to NONE. + if (level == MEMORY_TAG_LEVEL_TBI) { + level = MEMORY_TAG_LEVEL_NONE; + } + } else if (nativeSupportsTaggedPointers()) { + // TBI-but-not-MTE devices downgrade MTE modes to TBI. + // The idea is that if an app opts into full hardware tagging (MTE), it must be ok with + // the "fake" pointer tagging (TBI). + if (level == MEMORY_TAG_LEVEL_ASYNC || level == MEMORY_TAG_LEVEL_SYNC) { + level = MEMORY_TAG_LEVEL_TBI; + } + } else { + // Otherwise disable all tagging. + level = MEMORY_TAG_LEVEL_NONE; + } + + return level; + } + + private static int decideGwpAsanLevel( + @NonNull ApplicationInfo info, + @Nullable ProcessInfo processInfo, + @Nullable IPlatformCompat platformCompat) { + // Look at the process attribute first. + if (processInfo != null && processInfo.gwpAsanMode != ApplicationInfo.GWP_ASAN_DEFAULT) { + return processInfo.gwpAsanMode == ApplicationInfo.GWP_ASAN_ALWAYS + ? GWP_ASAN_LEVEL_ALWAYS + : GWP_ASAN_LEVEL_NEVER; + } + // Then at the application attribute. + if (info.getGwpAsanMode() != ApplicationInfo.GWP_ASAN_DEFAULT) { + return info.getGwpAsanMode() == ApplicationInfo.GWP_ASAN_ALWAYS + ? GWP_ASAN_LEVEL_ALWAYS + : GWP_ASAN_LEVEL_NEVER; + } + // If the app does not specify gwpAsanMode, the default behavior is lottery among the + // system apps, and disabled for user apps, unless overwritten by the compat feature. + if (isCompatChangeEnabled(GWP_ASAN, info, platformCompat, 0)) { + return GWP_ASAN_LEVEL_ALWAYS; + } + if ((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + return GWP_ASAN_LEVEL_LOTTERY; + } + return GWP_ASAN_LEVEL_NEVER; + } + + private static boolean enableNativeHeapZeroInit( + @NonNull ApplicationInfo info, + @Nullable ProcessInfo processInfo, + @Nullable IPlatformCompat platformCompat) { + // Look at the process attribute first. + if (processInfo != null + && processInfo.nativeHeapZeroInitialized != ApplicationInfo.ZEROINIT_DEFAULT) { + return processInfo.nativeHeapZeroInitialized == ApplicationInfo.ZEROINIT_ENABLED; + } + // Then at the application attribute. + if (info.getNativeHeapZeroInitialized() != ApplicationInfo.ZEROINIT_DEFAULT) { + return info.getNativeHeapZeroInitialized() == ApplicationInfo.ZEROINIT_ENABLED; + } + // Compat feature last. + if (isCompatChangeEnabled(NATIVE_HEAP_ZERO_INIT, info, platformCompat, 0)) { + return true; + } + return false; + } + + /** + * Returns Zygote runtimeFlags for memory safety features (MTE, GWP-ASan, nativeHeadZeroInit) + * for a given app. + */ + public static int getMemorySafetyRuntimeFlags( + @NonNull ApplicationInfo info, + @Nullable ProcessInfo processInfo, + @Nullable String instructionSet, + @Nullable IPlatformCompat platformCompat) { + int runtimeFlags = decideGwpAsanLevel(info, processInfo, platformCompat); + // If instructionSet is non-null, this indicates that the system_server is spawning a + // process with an ISA that may be different from its own. System (kernel and hardware) + // compatibility for these features is checked in the decideTaggingLevel in the + // system_server process (not the child process). As both MTE and TBI are only supported + // in aarch64, we can simply ensure that the new process is also aarch64. This prevents + // the mismatch where a 64-bit system server spawns a 32-bit child that thinks it should + // enable some tagging variant. Theoretically, a 32-bit system server could exist that + // spawns 64-bit processes, in which case the new process won't get any tagging. This is + // fine as we haven't seen this configuration in practice, and we can reasonable assume + // that if tagging is desired, the system server will be 64-bit. + if (instructionSet == null || instructionSet.equals("arm64")) { + runtimeFlags |= decideTaggingLevel(info, processInfo, platformCompat); + } + if (enableNativeHeapZeroInit(info, processInfo, platformCompat)) { + runtimeFlags |= NATIVE_HEAP_ZERO_INIT_ENABLED; + } + return runtimeFlags; + } + + /** + * Returns Zygote runtimeFlags for memory safety features (MTE, GWP-ASan, nativeHeadZeroInit) + * for a secondary zygote (AppZygote or WebViewZygote). + */ + public static int getMemorySafetyRuntimeFlagsForSecondaryZygote( + @NonNull ApplicationInfo info, @Nullable ProcessInfo processInfo) { + final IPlatformCompat platformCompat = + IPlatformCompat.Stub.asInterface( + ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); + int runtimeFlags = + getMemorySafetyRuntimeFlags( + info, processInfo, null /*instructionSet*/, platformCompat); + + // TBI ("fake" pointer tagging) in AppZygote is controlled by a separate compat feature. + if ((runtimeFlags & MEMORY_TAG_LEVEL_MASK) == MEMORY_TAG_LEVEL_TBI + && isCompatChangeEnabled( + NATIVE_HEAP_POINTER_TAGGING_SECONDARY_ZYGOTE, + info, + platformCompat, + Build.VERSION_CODES.S)) { + // Reset memory tag level to NONE. + runtimeFlags &= ~MEMORY_TAG_LEVEL_MASK; + runtimeFlags |= MEMORY_TAG_LEVEL_NONE; + } + return runtimeFlags; + } } diff --git a/core/java/com/android/internal/policy/ForceShowNavigationBarSettingsObserver.java b/core/java/com/android/internal/policy/ForceShowNavigationBarSettingsObserver.java index 3e72564bad5d..75dce5a278ff 100644 --- a/core/java/com/android/internal/policy/ForceShowNavigationBarSettingsObserver.java +++ b/core/java/com/android/internal/policy/ForceShowNavigationBarSettingsObserver.java @@ -26,7 +26,7 @@ import android.provider.Settings; /** * A ContentObserver for listening force show navigation bar relative setting keys: * - {@link Settings.Secure#NAVIGATION_MODE} - * - {@link Settings.Secure#NAV_BAR_KIDS_MODE} + * - {@link Settings.Secure#NAV_BAR_FORCE_VISIBLE} * * @hide */ @@ -52,7 +52,7 @@ public class ForceShowNavigationBarSettingsObserver extends ContentObserver { Settings.Secure.getUriFor(Settings.Secure.NAVIGATION_MODE), false, this, UserHandle.USER_ALL); r.registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.NAV_BAR_KIDS_MODE), + Settings.Secure.getUriFor(Settings.Secure.NAV_BAR_FORCE_VISIBLE), false, this, UserHandle.USER_ALL); } @@ -78,6 +78,6 @@ public class ForceShowNavigationBarSettingsObserver extends ContentObserver { return Settings.Secure.getIntForUser(mContext.getContentResolver(), Settings.Secure.NAVIGATION_MODE, 0, UserHandle.USER_CURRENT) == 0 && Settings.Secure.getIntForUser(mContext.getContentResolver(), - Settings.Secure.NAV_BAR_KIDS_MODE, 0, UserHandle.USER_CURRENT) == 1; + Settings.Secure.NAV_BAR_FORCE_VISIBLE, 0, UserHandle.USER_CURRENT) == 1; } } diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 099d1fc933d2..d629d66d1c31 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -159,7 +159,7 @@ oneway interface IStatusBar /** * Used to notify the authentication dialog that a biometric has been authenticated. */ - void onBiometricAuthenticated(); + void onBiometricAuthenticated(int modality); /** * Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc. */ diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index dcc1a7626a1b..9163b6d6215e 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -125,7 +125,7 @@ interface IStatusBarService int multiSensorConfig); // Used to notify the authentication dialog that a biometric has been authenticated - void onBiometricAuthenticated(); + void onBiometricAuthenticated(int modality); // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc void onBiometricHelp(int modality, String message); // Used to show an error - the dialog will dismiss after a certain amount of time diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 597167026d19..5b7092cabfcb 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -346,7 +346,7 @@ enum RuntimeFlags : uint32_t { GWP_ASAN_LEVEL_NEVER = 0 << 21, GWP_ASAN_LEVEL_LOTTERY = 1 << 21, GWP_ASAN_LEVEL_ALWAYS = 2 << 21, - NATIVE_HEAP_ZERO_INIT = 1 << 23, + NATIVE_HEAP_ZERO_INIT_ENABLED = 1 << 23, PROFILEABLE = 1 << 24, }; @@ -1709,13 +1709,13 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, // would be nice to have them for apps, we will have to wait until they are // proven out, have more efficient hardware, and/or apply them only to new // applications. - if (!(runtime_flags & RuntimeFlags::NATIVE_HEAP_ZERO_INIT)) { + if (!(runtime_flags & RuntimeFlags::NATIVE_HEAP_ZERO_INIT_ENABLED)) { mallopt(M_BIONIC_ZERO_INIT, 0); } // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART // runtime. - runtime_flags &= ~RuntimeFlags::NATIVE_HEAP_ZERO_INIT; + runtime_flags &= ~RuntimeFlags::NATIVE_HEAP_ZERO_INIT_ENABLED; bool forceEnableGwpAsan = false; switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) { diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 4c0ba8cbe304..6a421f007a76 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -385,7 +385,18 @@ message SecureSettingsProto { optional SettingProto multi_press_timeout = 38 [ (android.privacy).dest = DEST_AUTOMATIC ]; - optional SettingProto nav_bar_kids_mode = 91 [ (android.privacy).dest = DEST_AUTOMATIC ]; + message NavBar { + option (android.msg_privacy).dest = DEST_EXPLICIT; + + // Nav bar is forced to always be visible, even in immersive mode. + optional SettingProto nav_bar_force_visible = 1 [ (android.privacy).dest = DEST_AUTOMATIC ]; + // Indicates whether the device is in kids nav mode. + optional SettingProto nav_bar_kids_mode = 2 [ (android.privacy).dest = DEST_AUTOMATIC ]; + } + optional NavBar nav_bar = 92; + reserved 91; // Formerly nav_bar_kids_mode + reserved "nav_bar_kids_mode"; // Moved to message NavBar + optional SettingProto navigation_mode = 76 [ (android.privacy).dest = DEST_AUTOMATIC ]; message NfcPayment { @@ -668,5 +679,5 @@ message SecureSettingsProto { // Please insert fields in alphabetical order and group them into messages // if possible (to avoid reaching the method limit). - // Next tag = 92; + // Next tag = 93; } diff --git a/core/proto/android/server/biometrics.proto b/core/proto/android/server/biometrics.proto index fc9da90ccd59..ac9e3e001d50 100644 --- a/core/proto/android/server/biometrics.proto +++ b/core/proto/android/server/biometrics.proto @@ -91,23 +91,9 @@ message BiometricServiceStateProto { STATE_CLIENT_DIED_CANCELLING = 10; } - enum MultiSensorState { - // Initializing or not yet started. - MULTI_SENSOR_STATE_UNKNOWN = 0; - // Sensors are in the process of being transitioned and there is no active sensor. - MULTI_SENSOR_STATE_SWITCHING = 1; - // Face sensor is being used as the primary input. - MULTI_SENSOR_STATE_FACE_SCANNING = 2; - // Fingerprint sensor is being used as the primary input. - MULTI_SENSOR_STATE_FP_SCANNING = 3; - } - repeated SensorServiceStateProto sensor_service_states = 1; optional AuthSessionState auth_session_state = 2; - - // Additional session state information, when the device has multiple sensors. - optional MultiSensorState auth_session_multi_sensor_state = 3; } // Overall state for an instance of a <Biometric>Service, for example FingerprintService or diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto index e8f7b9343746..3929027a4e94 100644 --- a/core/proto/android/server/windowmanagerservice.proto +++ b/core/proto/android/server/windowmanagerservice.proto @@ -446,6 +446,7 @@ message WindowStateProto { optional bool has_compat_scale = 43; optional float global_scale = 44; repeated .android.graphics.RectProto keep_clear_areas = 45; + repeated .android.graphics.RectProto unrestricted_keep_clear_areas = 46; } message IdentifierProto { diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 73cdaba24789..d7744e377f12 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6926,6 +6926,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> + <service android:name="com.android.server.BinaryTransparencyService$UpdateMeasurementsJobService" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <service android:name="com.android.server.pm.PackageManagerShellCommandDataLoader" android:exported="false"> <intent-filter> diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index 23ec3ead959f..cf78646a466f 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -1307,55 +1307,6 @@ public class ChooserActivityTest { is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST)); } - /** - * The case when AppPrediction service is not defined in PackageManager is already covered - * as a test parameter {@link ChooserActivityTest#packageManagers}. This test is checking the - * case when the prediction service is defined but the component is not available on the device. - */ - @Test - public void testIsAppPredictionServiceAvailable() { - Intent sendIntent = createSendTextIntent(); - List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); - - final ChooserActivity activity = - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - if (activity.getPackageManager().getAppPredictionServicePackageName() == null) { - assertThat(activity.isAppPredictionServiceAvailable(), is(false)); - } else { - if (!shouldTestTogglingAppPredictionServiceAvailabilityAtRuntime()) { - return; - } - - // This isn't a toggle per-se, but isAppPredictionServiceAvailable only works in - // system (see comment in the method). - assertThat(activity.isAppPredictionServiceAvailable(), is(true)); - - ChooserActivityOverrideData.getInstance().resources = - Mockito.spy(activity.getResources()); - when( - ChooserActivityOverrideData - .getInstance() - .resources - .getString( - getRuntimeResourceId( - "config_defaultAppPredictionService", - "string"))) - .thenReturn("ComponentNameThatDoesNotExist"); - - assertThat(activity.isAppPredictionServiceAvailable(), is(false)); - } - } - @Test public void testConvertToChooserTarget_predictionService() { Intent sendIntent = createSendTextIntent(); diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java index 33b09b8831ce..55f205bb14a6 100644 --- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java +++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java @@ -690,6 +690,14 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { } } + /** + * Gets the total duration of the animation + * @hide + */ + public long getTotalDuration() { + return mAnimatorSet.getTotalDuration(); + } + private static class AnimatedVectorDrawableState extends ConstantState { @Config int mChangingConfigurations; VectorDrawable mVectorDrawable; @@ -1074,6 +1082,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { boolean isInfinite(); void pause(); void resume(); + long getTotalDuration(); } private static class VectorDrawableAnimatorUI implements VectorDrawableAnimator { @@ -1085,6 +1094,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { // setup by init(). private ArrayList<AnimatorListener> mListenerArray = null; private boolean mIsInfinite = false; + private long mTotalDuration; VectorDrawableAnimatorUI(@NonNull AnimatedVectorDrawable drawable) { mDrawable = drawable; @@ -1100,7 +1110,8 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { // Keep a deep copy of the set, such that set can be still be constantly representing // the static content from XML file. mSet = set.clone(); - mIsInfinite = mSet.getTotalDuration() == Animator.DURATION_INFINITE; + mTotalDuration = mSet.getTotalDuration(); + mIsInfinite = mTotalDuration == Animator.DURATION_INFINITE; // If there are listeners added before calling init(), now they should be setup. if (mListenerArray != null && !mListenerArray.isEmpty()) { @@ -1219,6 +1230,11 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { private void invalidateOwningView() { mDrawable.invalidateSelf(); } + + @Override + public long getTotalDuration() { + return mTotalDuration; + } } /** @@ -1249,6 +1265,7 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { private int mLastListenerId = 0; private final IntArray mPendingAnimationActions = new IntArray(); private final AnimatedVectorDrawable mDrawable; + private long mTotalDuration; VectorDrawableAnimatorRT(AnimatedVectorDrawable drawable) { mDrawable = drawable; @@ -1270,7 +1287,8 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { .getNativeTree(); nSetVectorDrawableTarget(mSetPtr, vectorDrawableTreePtr); mInitialized = true; - mIsInfinite = set.getTotalDuration() == Animator.DURATION_INFINITE; + mTotalDuration = set.getTotalDuration(); + mIsInfinite = mTotalDuration == Animator.DURATION_INFINITE; // Check reversible. mIsReversible = true; @@ -1796,6 +1814,11 @@ public class AnimatedVectorDrawable extends Drawable implements Animatable2 { } mPendingAnimationActions.clear(); } + + @Override + public long getTotalDuration() { + return mTotalDuration; + } } private static native long nCreateAnimatorSet(); diff --git a/graphics/java/android/graphics/drawable/AnimationDrawable.java b/graphics/java/android/graphics/drawable/AnimationDrawable.java index 8c3fa441cbb0..7fd2201e7108 100644 --- a/graphics/java/android/graphics/drawable/AnimationDrawable.java +++ b/graphics/java/android/graphics/drawable/AnimationDrawable.java @@ -424,6 +424,17 @@ public class AnimationDrawable extends DrawableContainer implements Runnable, An System.arraycopy(mDurations, 0, newDurations, 0, oldSize); mDurations = newDurations; } + + public long getTotalDuration() { + if (mDurations != null) { + int total = 0; + for (int dur : mDurations) { + total += dur; + } + return total; + } + return 0; + } } @Override @@ -435,6 +446,14 @@ public class AnimationDrawable extends DrawableContainer implements Runnable, An } } + /** + * Gets the total duration of the animation + * @hide + */ + public long getTotalDuration() { + return mAnimationState.getTotalDuration(); + } + private AnimationDrawable(AnimationState state, Resources res) { final AnimationState as = new AnimationState(state, this, res); setConstantState(as); diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java index 2e85b304ec47..31dd10a8ed53 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java @@ -66,6 +66,7 @@ import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.ECGenParameterSpec; +import java.security.spec.NamedParameterSpec; import java.security.spec.RSAKeyGenParameterSpec; import java.util.ArrayList; import java.util.Arrays; @@ -119,36 +120,42 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato private static final int RSA_MIN_KEY_SIZE = 512; private static final int RSA_MAX_KEY_SIZE = 8192; - private static final Map<String, Integer> SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE = + private static final Map<String, Integer> SUPPORTED_EC_CURVE_NAME_TO_SIZE = new HashMap<String, Integer>(); - private static final List<String> SUPPORTED_EC_NIST_CURVE_NAMES = new ArrayList<String>(); - private static final List<Integer> SUPPORTED_EC_NIST_CURVE_SIZES = new ArrayList<Integer>(); + private static final List<String> SUPPORTED_EC_CURVE_NAMES = new ArrayList<String>(); + private static final List<Integer> SUPPORTED_EC_CURVE_SIZES = new ArrayList<Integer>(); + private static final String CURVE_X_25519 = NamedParameterSpec.X25519.getName(); + private static final String CURVE_ED_25519 = NamedParameterSpec.ED25519.getName(); + static { // Aliases for NIST P-224 - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-224", 224); - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp224r1", 224); + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put("p-224", 224); + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put("secp224r1", 224); // Aliases for NIST P-256 - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-256", 256); - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp256r1", 256); - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("prime256v1", 256); + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put("p-256", 256); + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put("secp256r1", 256); + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put("prime256v1", 256); + // Aliases for Curve 25519 + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put(CURVE_X_25519.toLowerCase(Locale.US), 256); + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put(CURVE_ED_25519.toLowerCase(Locale.US), 256); // Aliases for NIST P-384 - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-384", 384); - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp384r1", 384); + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put("p-384", 384); + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put("secp384r1", 384); // Aliases for NIST P-521 - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("p-521", 521); - SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.put("secp521r1", 521); + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put("p-521", 521); + SUPPORTED_EC_CURVE_NAME_TO_SIZE.put("secp521r1", 521); - SUPPORTED_EC_NIST_CURVE_NAMES.addAll(SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.keySet()); - Collections.sort(SUPPORTED_EC_NIST_CURVE_NAMES); + SUPPORTED_EC_CURVE_NAMES.addAll(SUPPORTED_EC_CURVE_NAME_TO_SIZE.keySet()); + Collections.sort(SUPPORTED_EC_CURVE_NAMES); - SUPPORTED_EC_NIST_CURVE_SIZES.addAll( - new HashSet<Integer>(SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.values())); - Collections.sort(SUPPORTED_EC_NIST_CURVE_SIZES); + SUPPORTED_EC_CURVE_SIZES.addAll( + new HashSet<Integer>(SUPPORTED_EC_CURVE_NAME_TO_SIZE.values())); + Collections.sort(SUPPORTED_EC_CURVE_SIZES); } private final int mOriginalKeymasterAlgorithm; @@ -164,6 +171,7 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato private int mKeySizeBits; private SecureRandom mRng; private KeyDescriptor mAttestKeyDescriptor; + private String mEcCurveName; private int[] mKeymasterPurposes; private int[] mKeymasterBlockModes; @@ -177,12 +185,15 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato mOriginalKeymasterAlgorithm = keymasterAlgorithm; } - private @EcCurve int keySize2EcCurve(int keySizeBits) + private static @EcCurve int keySizeAndNameToEcCurve(int keySizeBits, String ecCurveName) throws InvalidAlgorithmParameterException { switch (keySizeBits) { case 224: return EcCurve.P_224; case 256: + if (isCurve25519(ecCurveName)) { + return EcCurve.CURVE_25519; + } return EcCurve.P_256; case 384: return EcCurve.P_384; @@ -247,7 +258,8 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato if (mKeySizeBits == -1) { mKeySizeBits = getDefaultKeySize(keymasterAlgorithm); } - checkValidKeySize(keymasterAlgorithm, mKeySizeBits, mSpec.isStrongBoxBacked()); + checkValidKeySize(keymasterAlgorithm, mKeySizeBits, mSpec.isStrongBoxBacked(), + mEcCurveName); if (spec.getKeystoreAlias() == null) { throw new InvalidAlgorithmParameterException("KeyStore entry alias not provided"); @@ -299,6 +311,7 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato mAttestKeyDescriptor = buildAndCheckAttestKeyDescriptor(spec); checkAttestKeyPurpose(spec); + checkCorrectKeyPurposeForCurve(spec); success = true; } finally { @@ -317,6 +330,42 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato } } + private void checkCorrectKeyPurposeForCurve(KeyGenParameterSpec spec) + throws InvalidAlgorithmParameterException { + // Validate the key usage purposes against the curve. x25519 should be + // key exchange only, ed25519 signing and attesting. + + if (!isCurve25519(mEcCurveName)) { + return; + } + + if (mEcCurveName.equalsIgnoreCase(CURVE_X_25519) + && spec.getPurposes() != KeyProperties.PURPOSE_AGREE_KEY) { + throw new InvalidAlgorithmParameterException( + "x25519 may only be used for key agreement."); + } else if (mEcCurveName.equalsIgnoreCase(CURVE_ED_25519) + && !hasOnlyAllowedPurposeForEd25519(spec.getPurposes())) { + throw new InvalidAlgorithmParameterException( + "ed25519 may not be used for key agreement."); + } + } + + private static boolean isCurve25519(String ecCurveName) { + if (ecCurveName == null) { + return false; + } + return ecCurveName.equalsIgnoreCase(CURVE_X_25519) + || ecCurveName.equalsIgnoreCase(CURVE_ED_25519); + } + + private static boolean hasOnlyAllowedPurposeForEd25519(@KeyProperties.PurposeEnum int purpose) { + final int allowedPurposes = KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY + | KeyProperties.PURPOSE_ATTEST_KEY; + boolean hasAllowedPurpose = (purpose & allowedPurposes) != 0; + boolean hasDisallowedPurpose = (purpose & ~allowedPurposes) != 0; + return hasAllowedPurpose && !hasDisallowedPurpose; + } + private KeyDescriptor buildAndCheckAttestKeyDescriptor(KeyGenParameterSpec spec) throws InvalidAlgorithmParameterException { if (spec.getAttestKeyAlias() != null) { @@ -473,6 +522,7 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato mRSAPublicExponent = null; mRng = null; mKeyStore = null; + mEcCurveName = null; } private void initAlgorithmSpecificParameters() throws InvalidAlgorithmParameterException { @@ -514,13 +564,13 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato case KeymasterDefs.KM_ALGORITHM_EC: if (algSpecificSpec instanceof ECGenParameterSpec) { ECGenParameterSpec ecSpec = (ECGenParameterSpec) algSpecificSpec; - String curveName = ecSpec.getName(); - Integer ecSpecKeySizeBits = SUPPORTED_EC_NIST_CURVE_NAME_TO_SIZE.get( - curveName.toLowerCase(Locale.US)); + mEcCurveName = ecSpec.getName(); + final Integer ecSpecKeySizeBits = SUPPORTED_EC_CURVE_NAME_TO_SIZE.get( + mEcCurveName.toLowerCase(Locale.US)); if (ecSpecKeySizeBits == null) { throw new InvalidAlgorithmParameterException( - "Unsupported EC curve name: " + curveName - + ". Supported: " + SUPPORTED_EC_NIST_CURVE_NAMES); + "Unsupported EC curve name: " + mEcCurveName + + ". Supported: " + SUPPORTED_EC_CURVE_NAMES); } if (mKeySizeBits == -1) { mKeySizeBits = ecSpecKeySizeBits; @@ -744,7 +794,7 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato if (mKeymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_EC) { params.add(KeyStore2ParameterUtils.makeEnum( - Tag.EC_CURVE, keySize2EcCurve(mKeySizeBits) + Tag.EC_CURVE, keySizeAndNameToEcCurve(mKeySizeBits, mEcCurveName) )); } @@ -864,7 +914,8 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato private static void checkValidKeySize( int keymasterAlgorithm, int keySize, - boolean isStrongBoxBacked) + boolean isStrongBoxBacked, + String mEcCurveName) throws InvalidAlgorithmParameterException { switch (keymasterAlgorithm) { case KeymasterDefs.KM_ALGORITHM_EC: @@ -873,9 +924,13 @@ public abstract class AndroidKeyStoreKeyPairGeneratorSpi extends KeyPairGenerato "Unsupported StrongBox EC key size: " + keySize + " bits. Supported: 256"); } - if (!SUPPORTED_EC_NIST_CURVE_SIZES.contains(keySize)) { + if (isStrongBoxBacked && isCurve25519(mEcCurveName)) { + throw new InvalidAlgorithmParameterException( + "Unsupported StrongBox EC: " + mEcCurveName); + } + if (!SUPPORTED_EC_CURVE_SIZES.contains(keySize)) { throw new InvalidAlgorithmParameterException("Unsupported EC key size: " - + keySize + " bits. Supported: " + SUPPORTED_EC_NIST_CURVE_SIZES); + + keySize + " bits. Supported: " + SUPPORTED_EC_CURVE_SIZES); } break; case KeymasterDefs.KM_ALGORITHM_RSA: diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java index 72a145fa6a05..358104fffbf6 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java @@ -66,6 +66,11 @@ public class AndroidKeyStoreProvider extends Provider { private static final String DESEDE_SYSTEM_PROPERTY = "ro.hardware.keystore_desede"; + // Conscrypt returns the Ed25519 OID as the JCA key algorithm. + private static final String ED25519_OID = "1.3.101.112"; + // Conscrypt returns "XDH" as the X25519 JCA key algorithm. + private static final String X25519_ALIAS = "XDH"; + /** @hide **/ public AndroidKeyStoreProvider() { super(PROVIDER_NAME, 1.0, "Android KeyStore security provider"); @@ -78,10 +83,16 @@ public class AndroidKeyStoreProvider extends Provider { // java.security.KeyPairGenerator put("KeyPairGenerator.EC", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$EC"); put("KeyPairGenerator.RSA", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$RSA"); + put("KeyPairGenerator." + X25519_ALIAS, + PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$RSA"); + put("KeyPairGenerator." + ED25519_OID, + PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$RSA"); // java.security.KeyFactory putKeyFactoryImpl("EC"); putKeyFactoryImpl("RSA"); + putKeyFactoryImpl(X25519_ALIAS); + putKeyFactoryImpl(ED25519_OID); // javax.crypto.KeyGenerator put("KeyGenerator.AES", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$AES"); @@ -219,12 +230,17 @@ public class AndroidKeyStoreProvider extends Provider { KeyStoreSecurityLevel securityLevel = iSecurityLevel; if (KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(jcaKeyAlgorithm)) { - return new AndroidKeyStoreECPublicKey(descriptor, metadata, iSecurityLevel, (ECPublicKey) publicKey); } else if (KeyProperties.KEY_ALGORITHM_RSA.equalsIgnoreCase(jcaKeyAlgorithm)) { return new AndroidKeyStoreRSAPublicKey(descriptor, metadata, iSecurityLevel, (RSAPublicKey) publicKey); + } else if (ED25519_OID.equalsIgnoreCase(jcaKeyAlgorithm)) { + //TODO(b/214203951) missing classes in conscrypt + throw new ProviderException("Curve " + ED25519_OID + " not supported yet"); + } else if (X25519_ALIAS.equalsIgnoreCase(jcaKeyAlgorithm)) { + //TODO(b/214203951) missing classes in conscrypt + throw new ProviderException("Curve " + X25519_ALIAS + " not supported yet"); } else { throw new ProviderException("Unsupported Android Keystore public key algorithm: " + jcaKeyAlgorithm); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 9b414681c0fa..8d5fdfbc8cb0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -52,6 +52,9 @@ import com.android.wm.shell.common.annotations.ShellMainThread; */ public class BackAnimationController implements RemoteCallable<BackAnimationController> { + private static final String BACK_PREDICTABILITY_PROP = "persist.debug.back_predictability"; + public static final boolean IS_ENABLED = SystemProperties + .getInt(BACK_PREDICTABILITY_PROP, 0) > 0; private static final String BACK_PREDICTABILITY_PROGRESS_THRESHOLD_PROP = "persist.debug.back_predictability_progress_threshold"; private static final int PROGRESS_THRESHOLD = SystemProperties 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 c6a68dc98da6..58f79f3600de 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 @@ -1608,7 +1608,9 @@ public class BubbleStackView extends FrameLayout Log.d(TAG, "addBubble: " + bubble); } - if (getBubbleCount() == 0 && shouldShowStackEdu()) { + final boolean firstBubble = getBubbleCount() == 0; + + if (firstBubble && shouldShowStackEdu()) { // Override the default stack position if we're showing user education. mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition()); } @@ -1621,7 +1623,7 @@ public class BubbleStackView extends FrameLayout new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), mPositioner.getBubbleSize())); - if (getBubbleCount() == 0) { + if (firstBubble) { mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); } // Set the dot position to the opposite of the side the stack is resting on, since the stack @@ -1652,6 +1654,10 @@ public class BubbleStackView extends FrameLayout bubble.cleanupViews(); } updateExpandedView(); + if (getBubbleCount() == 0 && !isExpanded()) { + // This is the last bubble and the stack is collapsed + updateStackPosition(); + } logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 8a482fbfa1c4..b52c8d118711 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -502,16 +502,24 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) { wct.setBounds(task1.token, mBounds1); + wct.setSmallestScreenWidthDp(task1.token, getSmallestWidthDp(mBounds1)); mWinBounds1.set(mBounds1); mWinToken1 = task1.token; } if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) { wct.setBounds(task2.token, mBounds2); + wct.setSmallestScreenWidthDp(task2.token, getSmallestWidthDp(mBounds2)); mWinBounds2.set(mBounds2); mWinToken2 = task2.token; } } + private int getSmallestWidthDp(Rect bounds) { + final int minWidth = Math.min(bounds.width(), bounds.height()); + final float density = mContext.getResources().getDisplayMetrics().density; + return (int) (minWidth / density); + } + /** * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And * restore shifted configuration bounds if it's no longer shifted. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index c94f3d197dea..0362b3fba36b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -699,7 +699,10 @@ public abstract class WMShellBaseModule { Context context, @ShellMainThread ShellExecutor shellExecutor ) { - return Optional.of( - new BackAnimationController(shellExecutor, context)); + if (BackAnimationController.IS_ENABLED) { + return Optional.of( + new BackAnimationController(shellExecutor, context)); + } + return Optional.empty(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java index 082fe9205be8..22dd9b953be7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java @@ -18,7 +18,6 @@ package com.android.wm.shell.splitscreen; import android.annotation.Nullable; import android.content.Context; -import android.graphics.Rect; import android.view.SurfaceSession; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -49,14 +48,13 @@ class MainStage extends StageTaskListener { return mIsActive; } - void activate(Rect rootBounds, WindowContainerTransaction wct, boolean includingTopTask) { + void activate(WindowContainerTransaction wct, boolean includingTopTask) { if (mIsActive) return; final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.setBounds(rootToken, rootBounds) - // Moving the root task to top after the child tasks were re-parented , or the root - // task cannot be visible and focused. - .reorder(rootToken, true /* onTop */); + // Moving the root task to top after the child tasks were re-parented , or the root + // task cannot be visible and focused. + wct.reorder(rootToken, true /* onTop */); if (includingTopTask) { wct.reparentTasks( null /* currentParent */, 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 76641f0e6c21..7318f4860b1c 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 @@ -354,8 +354,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.setDivideRatio(splitRatio); // Build a request WCT that will launch both apps such that task 0 is on the main stage // while task 1 is on the side stage. - mMainStage.activate(getMainStageBounds(), wct, false /* reparent */); - mSideStage.setBounds(getSideStageBounds(), wct); + mMainStage.activate(wct, false /* reparent */); + updateWindowBounds(mSplitLayout, wct); // Make sure the launch options will put tasks in the corresponding split roots addActivityOptions(mainOptions, mMainStage); @@ -470,13 +470,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.setDivideRatio(splitRatio); if (mMainStage.isActive()) { - mMainStage.moveToTop(getMainStageBounds(), wct); + mMainStage.moveToTop(wct); } else { // Build a request WCT that will launch both apps such that task 0 is on the main stage // while task 1 is on the side stage. - mMainStage.activate(getMainStageBounds(), wct, false /* reparent */); + mMainStage.activate(wct, false /* reparent */); } - mSideStage.moveToTop(getSideStageBounds(), wct); + mSideStage.moveToTop(wct); + updateWindowBounds(mSplitLayout, wct); // Make sure the launch options will put tasks in the corresponding split roots addActivityOptions(mainOptions, mMainStage); @@ -774,8 +775,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, setSideStagePosition(startPosition, wct); mSideStage.addTask(taskInfo, wct); } - mMainStage.activate(getMainStageBounds(), wct, true /* includingTopTask */); - mSideStage.moveToTop(getSideStageBounds(), wct); + mMainStage.activate(wct, true /* includingTopTask */); + mSideStage.moveToTop(wct); + updateWindowBounds(mSplitLayout, wct); } void finishEnterSplitScreen(SurfaceControl.Transaction t) { @@ -997,10 +999,9 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Exit to side stage if main stage no longer has children. exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED); } - } else if (isSideStage) { + } else if (isSideStage && !mMainStage.isActive()) { final WindowContainerTransaction wct = new WindowContainerTransaction(); mSplitLayout.init(); - // Make sure the main stage is active. prepareEnterSplitScreen(wct); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { 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 83534c178469..c5aab45f56fa 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 @@ -53,8 +53,8 @@ import java.io.PrintWriter; * Base class that handle common task org. related for split-screen stages. * Note that this class and its sub-class do not directly perform hierarchy operations. * They only serve to hold a collection of tasks and provide APIs like - * {@link #setBounds(Rect, WindowContainerTransaction)} for the centralized {@link StageCoordinator} - * to perform operations in-sync with other containers. + * {@link #addTask(ActivityManager.RunningTaskInfo, WindowContainerTransaction)} for the centralized + * {@link StageCoordinator} to perform hierarchy operations in-sync with other containers. * * @see StageCoordinator */ @@ -310,13 +310,9 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { wct.reparent(task.token, mRootTaskInfo.token, true /* onTop*/); } - void moveToTop(Rect rootBounds, WindowContainerTransaction wct) { + void moveToTop(WindowContainerTransaction wct) { final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.setBounds(rootToken, rootBounds).reorder(rootToken, true /* onTop */); - } - - void setBounds(Rect bounds, WindowContainerTransaction wct) { - wct.setBounds(mRootTaskInfo.token, bounds); + wct.reorder(rootToken, true /* onTop */); } void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index 025bcad07955..33aa018923a6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -69,6 +69,7 @@ import com.android.internal.graphics.palette.VariationalKMeansQuantizer; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.BaseIconFactory; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -115,8 +116,10 @@ public class SplashscreenContentDrawer { private final Handler mSplashscreenWorkerHandler; @VisibleForTesting final ColorCache mColorCache; + private final ShellExecutor mSplashScreenExecutor; - SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool) { + SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool, + ShellExecutor splashScreenExecutor) { mContext = context; mIconProvider = iconProvider; mTransactionPool = pool; @@ -129,6 +132,7 @@ public class SplashscreenContentDrawer { shellSplashscreenWorkerThread.start(); mSplashscreenWorkerHandler = shellSplashscreenWorkerThread.getThreadHandler(); mColorCache = new ColorCache(mContext, mSplashscreenWorkerHandler); + mSplashScreenExecutor = splashScreenExecutor; } /** @@ -397,7 +401,7 @@ public class SplashscreenContentDrawer { SplashScreenView build() { Drawable iconDrawable; - final int animationDuration; + final long animationDuration; if (mSuggestType == STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { // empty or legacy splash screen case @@ -455,8 +459,8 @@ public class SplashscreenContentDrawer { iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler); } else { mFinalIconDrawables = SplashscreenIconDrawableFactory.makeIconDrawable( - mTmpAttrs.mIconBgColor, mThemeColor, - iconDrawable, mDefaultIconSize, mFinalIconSize, mSplashscreenWorkerHandler); + mTmpAttrs.mIconBgColor, mThemeColor, iconDrawable, mDefaultIconSize, + mFinalIconSize, mSplashscreenWorkerHandler, mSplashScreenExecutor); } } @@ -516,7 +520,7 @@ public class SplashscreenContentDrawer { } private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable, - int animationDuration, Consumer<Runnable> uiThreadInitTask) { + long animationDuration, Consumer<Runnable> uiThreadInitTask) { Drawable foreground = null; Drawable background = null; if (iconDrawable != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java index 54281e0199e3..fdd5a1578f41 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java @@ -18,9 +18,7 @@ package com.android.wm.shell.startingsurface; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; @@ -36,6 +34,8 @@ import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Animatable; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Trace; @@ -44,6 +44,9 @@ import android.util.PathParser; import android.window.SplashScreenView; import com.android.internal.R; +import com.android.wm.shell.common.ShellExecutor; + +import java.util.function.LongConsumer; /** * Creating a lightweight Drawable object used for splash screen. @@ -60,14 +63,15 @@ public class SplashscreenIconDrawableFactory { */ static Drawable[] makeIconDrawable(@ColorInt int backgroundColor, @ColorInt int themeColor, @NonNull Drawable foregroundDrawable, int srcIconSize, int iconSize, - Handler splashscreenWorkerHandler) { + Handler splashscreenWorkerHandler, ShellExecutor splashScreenExecutor) { Drawable foreground; Drawable background = null; boolean drawBackground = backgroundColor != Color.TRANSPARENT && backgroundColor != themeColor; if (foregroundDrawable instanceof Animatable) { - foreground = new AnimatableIconAnimateListener(foregroundDrawable); + foreground = new AnimatableIconAnimateListener(foregroundDrawable, + splashScreenExecutor); } else if (foregroundDrawable instanceof AdaptiveIconDrawable) { // If the icon is Adaptive, we already use the icon background. drawBackground = false; @@ -266,99 +270,107 @@ public class SplashscreenIconDrawableFactory { */ public static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable implements SplashScreenView.IconAnimateListener { - private Animatable mAnimatableIcon; - private Animator mIconAnimator; + private final Animatable mAnimatableIcon; private boolean mAnimationTriggered; private AnimatorListenerAdapter mJankMonitoringListener; + private boolean mRunning; + private long mDuration; + private LongConsumer mStartListener; + private final ShellExecutor mSplashScreenExecutor; - AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable) { + AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable, + ShellExecutor splashScreenExecutor) { super(foregroundDrawable); - mForegroundDrawable.setCallback(mCallback); - } - - @Override - public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) { - mJankMonitoringListener = listener; - } - - @Override - public boolean prepareAnimate(long duration, Runnable startListener) { - mAnimatableIcon = (Animatable) mForegroundDrawable; - mIconAnimator = ValueAnimator.ofInt(0, 1); - mIconAnimator.setDuration(duration); - mIconAnimator.addListener(new Animator.AnimatorListener() { + Callback callback = new Callback() { @Override - public void onAnimationStart(Animator animation) { - if (startListener != null) { - startListener.run(); - } - try { - if (mJankMonitoringListener != null) { - mJankMonitoringListener.onAnimationStart(animation); - } - mAnimatableIcon.start(); - } catch (Exception ex) { - Log.e(TAG, "Error while running the splash screen animated icon", ex); - animation.cancel(); - } + public void invalidateDrawable(@NonNull Drawable who) { + invalidateSelf(); } @Override - public void onAnimationEnd(Animator animation) { - mAnimatableIcon.stop(); - if (mJankMonitoringListener != null) { - mJankMonitoringListener.onAnimationEnd(animation); - } + public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, + long when) { + scheduleSelf(what, when); } @Override - public void onAnimationCancel(Animator animation) { - mAnimatableIcon.stop(); - if (mJankMonitoringListener != null) { - mJankMonitoringListener.onAnimationCancel(animation); - } + public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { + unscheduleSelf(what); } + }; + mForegroundDrawable.setCallback(callback); + mSplashScreenExecutor = splashScreenExecutor; + mAnimatableIcon = (Animatable) mForegroundDrawable; + } - @Override - public void onAnimationRepeat(Animator animation) { - // do not repeat - mAnimatableIcon.stop(); - } - }); - return true; + @Override + public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) { + mJankMonitoringListener = listener; } @Override - public void stopAnimation() { - if (mIconAnimator != null && mIconAnimator.isRunning()) { - mIconAnimator.end(); - mJankMonitoringListener = null; - } + public void prepareAnimate(long duration, LongConsumer startListener) { + stopAnimation(); + mDuration = duration; + mStartListener = startListener; } - private final Callback mCallback = new Callback() { - @Override - public void invalidateDrawable(@NonNull Drawable who) { - invalidateSelf(); + private void startAnimation() { + if (mJankMonitoringListener != null) { + mJankMonitoringListener.onAnimationStart(null); + } + try { + mAnimatableIcon.start(); + } catch (Exception ex) { + Log.e(TAG, "Error while running the splash screen animated icon", ex); + mRunning = false; + if (mJankMonitoringListener != null) { + mJankMonitoringListener.onAnimationCancel(null); + } + if (mStartListener != null) { + mStartListener.accept(mDuration); + } + return; } + long animDuration = mDuration; + if (mAnimatableIcon instanceof AnimatedVectorDrawable + && ((AnimatedVectorDrawable) mAnimatableIcon).getTotalDuration() > 0) { + animDuration = ((AnimatedVectorDrawable) mAnimatableIcon).getTotalDuration(); + } else if (mAnimatableIcon instanceof AnimationDrawable + && ((AnimationDrawable) mAnimatableIcon).getTotalDuration() > 0) { + animDuration = ((AnimationDrawable) mAnimatableIcon).getTotalDuration(); + } + mRunning = true; + mSplashScreenExecutor.executeDelayed(this::stopAnimation, animDuration); + if (mStartListener != null) { + mStartListener.accept(Math.max(animDuration, 0)); + } + } - @Override - public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { - scheduleSelf(what, when); + private void onAnimationEnd() { + mAnimatableIcon.stop(); + if (mJankMonitoringListener != null) { + mJankMonitoringListener.onAnimationEnd(null); } + mStartListener = null; + mRunning = false; + } - @Override - public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { - unscheduleSelf(what); + @Override + public void stopAnimation() { + if (mRunning) { + mSplashScreenExecutor.removeCallbacks(this::stopAnimation); + onAnimationEnd(); + mJankMonitoringListener = null; } - }; + } private void ensureAnimationStarted() { if (mAnimationTriggered) { return; } - if (mIconAnimator != null && !mIconAnimator.isRunning()) { - mIconAnimator.start(); + if (!mRunning) { + startAnimation(); } mAnimationTriggered = true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java index 61cbf6e3c93c..04d6ef7f9505 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -148,7 +148,8 @@ public class StartingSurfaceDrawer { mContext = context; mDisplayManager = mContext.getSystemService(DisplayManager.class); mSplashScreenExecutor = splashScreenExecutor; - mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool); + mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool, + mSplashScreenExecutor); mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance()); mWindowManagerGlobal = WindowManagerGlobal.getInstance(); mDisplayManager.getDisplay(DEFAULT_DISPLAY); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 83d5f04b7cdb..daec336fc71e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -28,8 +28,10 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.app.ActivityManager; import android.content.res.Configuration; import android.graphics.Rect; +import android.window.WindowContainerTransaction; import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -38,6 +40,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.policy.DividerSnapAlgorithm; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayImeController; import org.junit.Before; @@ -56,6 +59,7 @@ public class SplitLayoutTests extends ShellTestCase { @Mock SplitWindowManager.ParentContainerCallbacks mCallbacks; @Mock DisplayImeController mDisplayImeController; @Mock ShellTaskOrganizer mTaskOrganizer; + @Mock WindowContainerTransaction mWct; @Captor ArgumentCaptor<Runnable> mRunnableCaptor; private SplitLayout mSplitLayout; @@ -149,6 +153,16 @@ public class SplitLayoutTests extends ShellTestCase { verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true)); } + @Test + public void testApplyTaskChanges_updatesSmallestScreenWidthDp() { + final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); + final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); + mSplitLayout.applyTaskChanges(mWct, task1, task2); + + verify(mWct).setSmallestScreenWidthDp(eq(task1.token), anyInt()); + verify(mWct).setSmallestScreenWidthDp(eq(task2.token), anyInt()); + } + private void waitDividerFlingFinished() { verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture()); mRunnableCaptor.getValue().run(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java index c9720671f49c..0639ad5d0a62 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java @@ -67,8 +67,7 @@ public class MainStageTests extends ShellTestCase { @Test public void testActiveDeactivate() { - mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct, - true /* reparent */); + mMainStage.activate(mWct, true /* reparent */); assertThat(mMainStage.isActive()).isTrue(); mMainStage.deactivate(mWct); 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 59c377a3e13d..19d2a7ef6f45 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 @@ -44,7 +44,6 @@ import static org.mockito.Mockito.mock; import android.annotation.NonNull; import android.app.ActivityManager; -import android.graphics.Rect; import android.os.IBinder; import android.os.RemoteException; import android.view.SurfaceControl; @@ -421,8 +420,7 @@ public class SplitTransitionTests extends ShellTestCase { mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); - mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction(), - true /* includingTopTask */); + mMainStage.activate(new WindowContainerTransaction(), true /* includingTopTask */); } private boolean containsSplitEnter(@NonNull WindowContainerTransaction wct) { diff --git a/media/jni/soundpool/Stream.cpp b/media/jni/soundpool/Stream.cpp index 50bb79ccaa0b..9ed8770a455c 100644 --- a/media/jni/soundpool/Stream.cpp +++ b/media/jni/soundpool/Stream.cpp @@ -15,6 +15,7 @@ */ //#define LOG_NDEBUG 0 +#include <utility> #define LOG_TAG "SoundPool::Stream" #include <utils/Log.h> #include <android/content/AttributionSourceState.h> @@ -309,13 +310,11 @@ void Stream::play_l(const std::shared_ptr<Sound>& sound, int32_t nextStreamID, } if (mAudioTrack == nullptr) { // mToggle toggles each time a track is started on a given stream. - // The toggle is concatenated with the Stream address and passed to AudioTrack - // as callback user data. This enables the detection of callbacks received from the old + // This enables the detection of callbacks received from the old // audio track while the new one is being started and avoids processing them with // wrong audio audio buffer size (mAudioBufferSize) auto toggle = mToggle ^ 1; // NOLINTNEXTLINE(performance-no-int-to-ptr) - void* userData = reinterpret_cast<void*>((uintptr_t)this | toggle); audio_channel_mask_t soundChannelMask = sound->getChannelMask(); // When sound contains a valid channel mask, use it as is. // Otherwise, use stream count to calculate channel mask. @@ -327,10 +326,11 @@ void Stream::play_l(const std::shared_ptr<Sound>& sound, int32_t nextStreamID, android::content::AttributionSourceState attributionSource; attributionSource.packageName = mStreamManager->getOpPackageName(); attributionSource.token = sp<BBinder>::make(); + mCallback = sp<StreamCallback>::make(this, toggle), // TODO b/182469354 make consistent with AudioRecord, add util for native source mAudioTrack = new AudioTrack(streamType, sampleRate, sound->getFormat(), channelMask, sound->getIMemory(), AUDIO_OUTPUT_FLAG_FAST, - staticCallback, userData, + mCallback, 0 /*default notification frames*/, AUDIO_SESSION_ALLOCATE, AudioTrack::TRANSFER_DEFAULT, nullptr /*offloadInfo*/, attributionSource, @@ -375,16 +375,55 @@ void Stream::play_l(const std::shared_ptr<Sound>& sound, int32_t nextStreamID, mStreamID = nextStreamID; // prefer this to be the last, as it is an atomic sync point } -/* static */ -void Stream::staticCallback(int event, void* user, void* info) -{ - const auto userAsInt = (uintptr_t)user; - // NOLINTNEXTLINE(performance-no-int-to-ptr) - auto stream = reinterpret_cast<Stream*>(userAsInt & ~1); - stream->callback(event, info, int(userAsInt & 1), 0 /* tries */); +int Stream::getCorrespondingStreamID() { + std::lock_guard lock(mLock); + return static_cast<int>(mAudioTrack ? mStreamID : getPairStream()->mStreamID); +} +size_t Stream::StreamCallback::onMoreData(const AudioTrack::Buffer&) { + ALOGW("%s streamID %d Unexpected EVENT_MORE_DATA for static track", + __func__, mStream->getCorrespondingStreamID()); + return 0; +} + +void Stream::StreamCallback::onUnderrun() { + ALOGW("%s streamID %d Unexpected EVENT_UNDERRUN for static track", + __func__, mStream->getCorrespondingStreamID()); +} + +void Stream::StreamCallback::onLoopEnd(int32_t) { + ALOGV("%s streamID %d EVENT_LOOP_END", __func__, mStream->getCorrespondingStreamID()); +} + +void Stream::StreamCallback::onMarker(uint32_t) { + ALOGW("%s streamID %d Unexpected EVENT_MARKER for static track", + __func__, mStream->getCorrespondingStreamID()); +} + +void Stream::StreamCallback::onNewPos(uint32_t) { + ALOGW("%s streamID %d Unexpected EVENT_NEW_POS for static track", + __func__, mStream->getCorrespondingStreamID()); } -void Stream::callback(int event, void* info, int toggle, int tries) +void Stream::StreamCallback::onBufferEnd() { + mStream->onBufferEnd(mToggle, 0); +} + +void Stream::StreamCallback::onNewIAudioTrack() { + ALOGV("%s streamID %d NEW_IAUDIOTRACK", __func__, mStream->getCorrespondingStreamID()); +} + +void Stream::StreamCallback::onStreamEnd() { + ALOGW("%s streamID %d Unexpected EVENT_STREAM_END for static track", + __func__, mStream->getCorrespondingStreamID()); +} + +size_t Stream::StreamCallback::onCanWriteMoreData(const AudioTrack::Buffer&) { + ALOGW("%s streamID %d Unexpected EVENT_CAN_WRITE_MORE_DATA for static track", + __func__, mStream->getCorrespondingStreamID()); + return 0; +} + +void Stream::onBufferEnd(int toggle, int tries) { int32_t activeStreamIDToRestart = 0; { @@ -400,7 +439,7 @@ void Stream::callback(int event, void* info, int toggle, int tries) if (tries < 3) { lock.unlock(); ALOGV("%s streamID %d going to pair stream", __func__, (int)mStreamID); - getPairStream()->callback(event, info, toggle, tries + 1); + getPairStream()->onBufferEnd(toggle, tries + 1); } else { ALOGW("%s streamID %d cannot find track", __func__, (int)mStreamID); } @@ -410,31 +449,10 @@ void Stream::callback(int event, void* info, int toggle, int tries) ALOGD("%s streamID %d wrong toggle", __func__, (int)mStreamID); return; } - switch (event) { - case AudioTrack::EVENT_MORE_DATA: - ALOGW("%s streamID %d Invalid EVENT_MORE_DATA for static track", - __func__, (int)mStreamID); - break; - case AudioTrack::EVENT_UNDERRUN: - ALOGW("%s streamID %d Invalid EVENT_UNDERRUN for static track", - __func__, (int)mStreamID); - break; - case AudioTrack::EVENT_BUFFER_END: - ALOGV("%s streamID %d EVENT_BUFFER_END", __func__, (int)mStreamID); - if (mState != IDLE) { - activeStreamIDToRestart = mStreamID; - mStopTimeNs = systemTime(); - } - break; - case AudioTrack::EVENT_LOOP_END: - ALOGV("%s streamID %d EVENT_LOOP_END", __func__, (int)mStreamID); - break; - case AudioTrack::EVENT_NEW_IAUDIOTRACK: - ALOGV("%s streamID %d NEW_IAUDIOTRACK", __func__, (int)mStreamID); - break; - default: - ALOGW("%s streamID %d Invalid event %d", __func__, (int)mStreamID, event); - break; + ALOGV("%s streamID %d EVENT_BUFFER_END", __func__, (int)mStreamID); + if (mState != IDLE) { + activeStreamIDToRestart = mStreamID; + mStopTimeNs = systemTime(); } } // lock ends here. This is on the callback thread, no need to be precise. if (activeStreamIDToRestart > 0) { diff --git a/media/jni/soundpool/Stream.h b/media/jni/soundpool/Stream.h index aa0eef5bc66e..0054eeca529a 100644 --- a/media/jni/soundpool/Stream.h +++ b/media/jni/soundpool/Stream.h @@ -124,6 +124,35 @@ public: // This never changes. See top of header. Stream* getPairStream() const; + // Stream ID of ourselves, or the pair depending on who holds the AudioTrack + int getCorrespondingStreamID(); + +protected: + // AudioTrack callback interface implementation + class StreamCallback : public AudioTrack::IAudioTrackCallback { + public: + StreamCallback(Stream * stream, bool toggle) : mStream(stream), mToggle(toggle) {} + size_t onMoreData(const AudioTrack::Buffer& buffer) override; + void onUnderrun() override; + void onLoopEnd(int32_t loopsRemaining) override; + void onMarker(uint32_t markerPosition) override; + void onNewPos(uint32_t newPos) override; + void onBufferEnd() override; + void onNewIAudioTrack() override; + void onStreamEnd() override; + size_t onCanWriteMoreData(const AudioTrack::Buffer& buffer) override; + + // Holding a raw ptr is technically unsafe, but, Stream objects persist + // through the lifetime of the StreamManager through the use of a + // unique_ptr<Stream[]>. Ensuring lifetime will cause us to give up + // locality as well as pay RefBase/sp performance cost, which we are + // unwilling to do. Non-owning refs to unique_ptrs are idiomatically raw + // ptrs, as below. + Stream * const mStream; + const bool mToggle; + }; + + sp<StreamCallback> mCallback; private: // garbage is used to release tracks and data outside of any lock. void play_l(const std::shared_ptr<Sound>& sound, int streamID, @@ -133,9 +162,7 @@ private: void setVolume_l(float leftVolume, float rightVolume) REQUIRES(mLock); // For use with AudioTrack callback. - static void staticCallback(int event, void* user, void* info); - void callback(int event, void* info, int toggle, int tries) - NO_THREAD_SAFETY_ANALYSIS; // uses unique_lock + void onBufferEnd(int toggle, int tries) NO_THREAD_SAFETY_ANALYSIS; // StreamManager should be set on construction and not changed. // release mLock before calling into StreamManager diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/BluetoothProfileConnectionInfoTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/BluetoothProfileConnectionInfoTest.java deleted file mode 100644 index f23794b50543..000000000000 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/BluetoothProfileConnectionInfoTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2021 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.mediaframeworktest.unit; - -import static org.junit.Assert.assertEquals; - -import android.bluetooth.BluetoothProfile; -import android.media.BluetoothProfileConnectionInfo; - -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -public class BluetoothProfileConnectionInfoTest { - - @Test - public void testCoverageA2dp() { - final boolean supprNoisy = false; - final int volume = 42; - final BluetoothProfileConnectionInfo info = BluetoothProfileConnectionInfo - .createA2dpInfo(supprNoisy, volume); - assertEquals(info.getProfile(), BluetoothProfile.A2DP); - assertEquals(info.isSuppressNoisyIntent(), supprNoisy); - assertEquals(info.getVolume(), volume); - } - - @Test - public void testCoverageA2dpSink() { - final int volume = 42; - final BluetoothProfileConnectionInfo info = BluetoothProfileConnectionInfo - .createA2dpSinkInfo(volume); - assertEquals(info.getProfile(), BluetoothProfile.A2DP_SINK); - assertEquals(info.getVolume(), volume); - } - - @Test - public void testCoveragehearingAid() { - final boolean supprNoisy = true; - final BluetoothProfileConnectionInfo info = BluetoothProfileConnectionInfo - .createHearingAidInfo(supprNoisy); - assertEquals(info.getProfile(), BluetoothProfile.HEARING_AID); - assertEquals(info.isSuppressNoisyIntent(), supprNoisy); - } - - @Test - public void testCoverageLeAudio() { - final boolean supprNoisy = false; - final boolean isLeOutput = true; - final BluetoothProfileConnectionInfo info = BluetoothProfileConnectionInfo - .createLeAudioInfo(supprNoisy, isLeOutput); - assertEquals(info.getProfile(), BluetoothProfile.LE_AUDIO); - assertEquals(info.isSuppressNoisyIntent(), supprNoisy); - assertEquals(info.isLeOutput(), isLeOutput); - } -} - diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index 0c360519ceb2..65428de95519 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -48,6 +48,7 @@ private: static APerformanceHintManager* create(sp<IHintManager> iHintManager); sp<IHintManager> mHintManager; + const sp<IBinder> mToken = sp<BBinder>::make(); const int64_t mPreferredRateNanos; }; @@ -119,11 +120,10 @@ APerformanceHintManager* APerformanceHintManager::create(sp<IHintManager> manage APerformanceHintSession* APerformanceHintManager::createSession( const int32_t* threadIds, size_t size, int64_t initialTargetWorkDurationNanos) { - sp<IBinder> token = sp<BBinder>::make(); std::vector<int32_t> tids(threadIds, threadIds + size); sp<IHintSession> session; binder::Status ret = - mHintManager->createHintSession(token, tids, initialTargetWorkDurationNanos, &session); + mHintManager->createHintSession(mToken, tids, initialTargetWorkDurationNanos, &session); if (!ret.isOk() || !session) { return nullptr; } diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java index 61b8911acea4..0cb2c0b22a4c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPhotoController.java @@ -21,7 +21,8 @@ import android.content.ClipData; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.database.Cursor; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; @@ -30,15 +31,15 @@ import android.graphics.Paint; import android.graphics.RectF; import android.media.ExifInterface; import android.net.Uri; -import android.os.AsyncTask; import android.os.StrictMode; -import android.provider.ContactsContract; import android.provider.MediaStore; import android.util.EventLog; import android.util.Log; import androidx.core.content.FileProvider; +import com.android.settingslib.utils.ThreadUtils; + import libcore.io.Streams; import java.io.File; @@ -47,39 +48,64 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.concurrent.ExecutionException; class AvatarPhotoController { + + interface AvatarUi { + boolean isFinishing(); + + void returnUriResult(Uri uri); + + void startActivityForResult(Intent intent, int resultCode); + + boolean startSystemActivityForResult(Intent intent, int resultCode); + + int getPhotoSize(); + } + + interface ContextInjector { + File getCacheDir(); + + Uri createTempImageUri(File parentDir, String fileName, boolean purge); + + ContentResolver getContentResolver(); + } + private static final String TAG = "AvatarPhotoController"; - private static final int REQUEST_CODE_CHOOSE_PHOTO = 1001; - private static final int REQUEST_CODE_TAKE_PHOTO = 1002; - private static final int REQUEST_CODE_CROP_PHOTO = 1003; - // in rare cases we get a null Cursor when querying for DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI - // so we need a default photo size - private static final int DEFAULT_PHOTO_SIZE = 500; + static final int REQUEST_CODE_CHOOSE_PHOTO = 1001; + static final int REQUEST_CODE_TAKE_PHOTO = 1002; + static final int REQUEST_CODE_CROP_PHOTO = 1003; private static final String IMAGES_DIR = "multi_user"; + private static final String PRE_CROP_PICTURE_FILE_NAME = "PreCropEditUserPhoto.jpg"; private static final String CROP_PICTURE_FILE_NAME = "CropEditUserPhoto.jpg"; private static final String TAKE_PICTURE_FILE_NAME = "TakeEditUserPhoto.jpg"; private final int mPhotoSize; - private final AvatarPickerActivity mActivity; - private final String mFileAuthority; + private final AvatarUi mAvatarUi; + private final ContextInjector mContextInjector; private final File mImagesDir; + private final Uri mPreCropPictureUri; private final Uri mCropPictureUri; private final Uri mTakePictureUri; - AvatarPhotoController(AvatarPickerActivity activity, boolean waiting, String fileAuthority) { - mActivity = activity; - mFileAuthority = fileAuthority; + AvatarPhotoController(AvatarUi avatarUi, ContextInjector contextInjector, boolean waiting) { + mAvatarUi = avatarUi; + mContextInjector = contextInjector; - mImagesDir = new File(activity.getCacheDir(), IMAGES_DIR); + mImagesDir = new File(mContextInjector.getCacheDir(), IMAGES_DIR); mImagesDir.mkdir(); - mCropPictureUri = createTempImageUri(activity, CROP_PICTURE_FILE_NAME, !waiting); - mTakePictureUri = createTempImageUri(activity, TAKE_PICTURE_FILE_NAME, !waiting); - mPhotoSize = getPhotoSize(activity); + mPreCropPictureUri = mContextInjector + .createTempImageUri(mImagesDir, PRE_CROP_PICTURE_FILE_NAME, !waiting); + mCropPictureUri = + mContextInjector.createTempImageUri(mImagesDir, CROP_PICTURE_FILE_NAME, !waiting); + mTakePictureUri = + mContextInjector.createTempImageUri(mImagesDir, TAKE_PICTURE_FILE_NAME, !waiting); + mPhotoSize = mAvatarUi.getPhotoSize(); } /** @@ -102,16 +128,12 @@ class AvatarPhotoController { switch (requestCode) { case REQUEST_CODE_CROP_PHOTO: - mActivity.returnUriResult(pictureUri); + mAvatarUi.returnUriResult(pictureUri); return true; case REQUEST_CODE_TAKE_PHOTO: case REQUEST_CODE_CHOOSE_PHOTO: if (mTakePictureUri.equals(pictureUri)) { - if (PhotoCapabilityUtils.canCropPhoto(mActivity)) { - cropPhoto(); - } else { - onPhotoNotCropped(pictureUri); - } + cropPhoto(pictureUri); } else { copyAndCropPhoto(pictureUri); } @@ -123,55 +145,52 @@ class AvatarPhotoController { void takePhoto() { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE_SECURE); appendOutputExtra(intent, mTakePictureUri); - mActivity.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); + mAvatarUi.startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO); } void choosePhoto() { Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES, null); intent.setType("image/*"); - mActivity.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO); + mAvatarUi.startActivityForResult(intent, REQUEST_CODE_CHOOSE_PHOTO); } private void copyAndCropPhoto(final Uri pictureUri) { - // TODO: Replace AsyncTask - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - final ContentResolver cr = mActivity.getContentResolver(); + try { + ThreadUtils.postOnBackgroundThread(() -> { + final ContentResolver cr = mContextInjector.getContentResolver(); try (InputStream in = cr.openInputStream(pictureUri); - OutputStream out = cr.openOutputStream(mTakePictureUri)) { + OutputStream out = cr.openOutputStream(mPreCropPictureUri)) { Streams.copy(in, out); } catch (IOException e) { Log.w(TAG, "Failed to copy photo", e); + return; } - return null; - } - - @Override - protected void onPostExecute(Void result) { - if (!mActivity.isFinishing() && !mActivity.isDestroyed()) { - cropPhoto(); - } - } - }.execute(); + ThreadUtils.postOnMainThread(() -> { + if (!mAvatarUi.isFinishing()) { + cropPhoto(mPreCropPictureUri); + } + }); + }).get(); + } catch (InterruptedException | ExecutionException e) { + Log.e(TAG, "Error performing copy-and-crop", e); + } } - private void cropPhoto() { + private void cropPhoto(final Uri pictureUri) { // TODO: Use a public intent, when there is one. Intent intent = new Intent("com.android.camera.action.CROP"); - intent.setDataAndType(mTakePictureUri, "image/*"); + intent.setDataAndType(pictureUri, "image/*"); appendOutputExtra(intent, mCropPictureUri); appendCropExtras(intent); - if (intent.resolveActivity(mActivity.getPackageManager()) != null) { - try { - StrictMode.disableDeathOnFileUriExposure(); - mActivity.startActivityForResult(intent, REQUEST_CODE_CROP_PHOTO); - } finally { - StrictMode.enableDeathOnFileUriExposure(); + try { + StrictMode.disableDeathOnFileUriExposure(); + if (mAvatarUi.startSystemActivityForResult(intent, REQUEST_CODE_CROP_PHOTO)) { + return; } - } else { - onPhotoNotCropped(mTakePictureUri); + } finally { + StrictMode.enableDeathOnFileUriExposure(); } + onPhotoNotCropped(pictureUri); } private void appendOutputExtra(Intent intent, Uri pictureUri) { @@ -192,24 +211,22 @@ class AvatarPhotoController { } private void onPhotoNotCropped(final Uri data) { - // TODO: Replace AsyncTask to avoid possible memory leaks and handle configuration change - new AsyncTask<Void, Void, Bitmap>() { - @Override - protected Bitmap doInBackground(Void... params) { + try { + ThreadUtils.postOnBackgroundThread(() -> { // Scale and crop to a square aspect ratio Bitmap croppedImage = Bitmap.createBitmap(mPhotoSize, mPhotoSize, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(croppedImage); Bitmap fullImage; try { - InputStream imageStream = mActivity.getContentResolver() + InputStream imageStream = mContextInjector.getContentResolver() .openInputStream(data); fullImage = BitmapFactory.decodeStream(imageStream); } catch (FileNotFoundException fe) { - return null; + return; } if (fullImage != null) { - int rotation = getRotation(mActivity, data); + int rotation = getRotation(data); final int squareSize = Math.min(fullImage.getWidth(), fullImage.getHeight()); final int left = (fullImage.getWidth() - squareSize) / 2; @@ -222,29 +239,27 @@ class AvatarPhotoController { matrix.setRectToRect(rectSource, rectDest, Matrix.ScaleToFit.CENTER); matrix.postRotate(rotation, mPhotoSize / 2f, mPhotoSize / 2f); canvas.drawBitmap(fullImage, matrix, new Paint()); - return croppedImage; - } else { - // Bah! Got nothin. - return null; - } - } + saveBitmapToFile(croppedImage, new File(mImagesDir, CROP_PICTURE_FILE_NAME)); - @Override - protected void onPostExecute(Bitmap bitmap) { - saveBitmapToFile(bitmap, new File(mImagesDir, CROP_PICTURE_FILE_NAME)); - mActivity.returnUriResult(mCropPictureUri); - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + ThreadUtils.postOnMainThread(() -> { + mAvatarUi.returnUriResult(mCropPictureUri); + }); + } + }).get(); + } catch (InterruptedException | ExecutionException e) { + Log.e(TAG, "Error performing internal crop", e); + } } /** * Reads the image's exif data and determines the rotation degree needed to display the image * in portrait mode. */ - private int getRotation(Context context, Uri selectedImage) { + private int getRotation(Uri selectedImage) { int rotation = -1; try { - InputStream imageStream = context.getContentResolver().openInputStream(selectedImage); + InputStream imageStream = + mContextInjector.getContentResolver().openInputStream(selectedImage); ExifInterface exif = new ExifInterface(imageStream); rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1); } catch (IOException exception) { @@ -274,24 +289,74 @@ class AvatarPhotoController { } } - private static int getPhotoSize(Context context) { - try (Cursor cursor = context.getContentResolver().query( - ContactsContract.DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, - new String[]{ContactsContract.DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null)) { - if (cursor != null) { - cursor.moveToFirst(); - return cursor.getInt(0); - } else { - return DEFAULT_PHOTO_SIZE; + static class AvatarUiImpl implements AvatarUi { + private final AvatarPickerActivity mActivity; + + AvatarUiImpl(AvatarPickerActivity activity) { + mActivity = activity; + } + + @Override + public boolean isFinishing() { + return mActivity.isFinishing() || mActivity.isDestroyed(); + } + + @Override + public void returnUriResult(Uri uri) { + mActivity.returnUriResult(uri); + } + + @Override + public void startActivityForResult(Intent intent, int resultCode) { + mActivity.startActivityForResult(intent, resultCode); + } + + @Override + public boolean startSystemActivityForResult(Intent intent, int code) { + ActivityInfo info = intent.resolveActivityInfo(mActivity.getPackageManager(), + PackageManager.MATCH_SYSTEM_ONLY); + if (info == null) { + Log.w(TAG, "No system package activity could be found for code " + code); + return false; } + intent.setPackage(info.packageName); + mActivity.startActivityForResult(intent, code); + return true; + } + + @Override + public int getPhotoSize() { + return mActivity.getResources() + .getDimensionPixelSize(com.android.internal.R.dimen.user_icon_size); } } - private Uri createTempImageUri(Context context, String fileName, boolean purge) { - final File fullPath = new File(mImagesDir, fileName); - if (purge) { - fullPath.delete(); + static class ContextInjectorImpl implements ContextInjector { + private final Context mContext; + private final String mFileAuthority; + + ContextInjectorImpl(Context context, String fileAuthority) { + mContext = context; + mFileAuthority = fileAuthority; + } + + @Override + public File getCacheDir() { + return mContext.getCacheDir(); + } + + @Override + public Uri createTempImageUri(File parentDir, String fileName, boolean purge) { + final File fullPath = new File(parentDir, fileName); + if (purge) { + fullPath.delete(); + } + return FileProvider.getUriForFile(mContext, mFileAuthority, fullPath); + } + + @Override + public ContentResolver getContentResolver() { + return mContext.getContentResolver(); } - return FileProvider.getUriForFile(context, mFileAuthority, fullPath); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java index 1e1dfae9f7ac..75bb70a123c3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/AvatarPickerActivity.java @@ -95,7 +95,9 @@ public class AvatarPickerActivity extends Activity { restoreState(savedInstanceState); mAvatarPhotoController = new AvatarPhotoController( - this, mWaitingForActivityResult, getFileAuthority()); + new AvatarPhotoController.AvatarUiImpl(this), + new AvatarPhotoController.ContextInjectorImpl(this, getFileAuthority()), + mWaitingForActivityResult); } private void setUpButtons() { diff --git a/packages/SettingsLib/tests/integ/AndroidManifest.xml b/packages/SettingsLib/tests/integ/AndroidManifest.xml index da808dd54141..2a4dfdd84c63 100644 --- a/packages/SettingsLib/tests/integ/AndroidManifest.xml +++ b/packages/SettingsLib/tests/integ/AndroidManifest.xml @@ -25,10 +25,19 @@ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> - <application> <uses-library android:name="android.test.runner" /> <activity android:name=".drawer.SettingsDrawerActivityTest$TestActivity"/> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="com.android.settingslib.test" + android:grantUriPermissions="true" + android:exported="false"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/file_paths" /> + </provider> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/packages/SettingsLib/tests/integ/res/xml/file_paths.xml b/packages/SettingsLib/tests/integ/res/xml/file_paths.xml new file mode 100644 index 000000000000..ccd11a46e429 --- /dev/null +++ b/packages/SettingsLib/tests/integ/res/xml/file_paths.xml @@ -0,0 +1,20 @@ +<!-- + ~ 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. + --> + +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Offer access to files under Context.getCacheDir() --> + <cache-path name="my_cache" /> +</paths> diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AvatarPhotoControllerTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AvatarPhotoControllerTest.java new file mode 100644 index 000000000000..9ebdba300266 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/users/AvatarPhotoControllerTest.java @@ -0,0 +1,299 @@ +/* + * 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.settingslib.users; + +import static com.android.settingslib.users.AvatarPhotoController.REQUEST_CODE_CHOOSE_PHOTO; +import static com.android.settingslib.users.AvatarPhotoController.REQUEST_CODE_CROP_PHOTO; +import static com.android.settingslib.users.AvatarPhotoController.REQUEST_CODE_TAKE_PHOTO; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.provider.MediaStore; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +@RunWith(AndroidJUnit4.class) +public class AvatarPhotoControllerTest { + + private static final long TIMEOUT_MILLIS = 5000; + private static final int PHOTO_SIZE = 200; + + @Mock AvatarPhotoController.AvatarUi mMockAvatarUi; + + private File mImagesDir; + private AvatarPhotoController mController; + private Uri mTakePhotoUri = Uri.parse( + "content://com.android.settingslib.test/my_cache/multi_user/TakeEditUserPhoto.jpg"); + private Uri mCropPhotoUri = Uri.parse( + "content://com.android.settingslib.test/my_cache/multi_user/CropEditUserPhoto.jpg"); + private Context mContext = InstrumentationRegistry.getTargetContext(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockAvatarUi.getPhotoSize()).thenReturn(PHOTO_SIZE); + when(mMockAvatarUi.startSystemActivityForResult(any(), anyInt())).thenReturn(true); + + mImagesDir = new File( + InstrumentationRegistry.getTargetContext().getCacheDir(), "multi_user"); + mImagesDir.mkdir(); + + AvatarPhotoController.ContextInjector contextInjector = + new AvatarPhotoController.ContextInjectorImpl( + InstrumentationRegistry.getTargetContext(), "com.android.settingslib.test"); + mController = new AvatarPhotoController(mMockAvatarUi, contextInjector, false); + } + + @After + public void tearDown() { + mImagesDir.delete(); + } + + @Test + public void takePhotoHasCorrectIntentAndResultCode() { + mController.takePhoto(); + + verifyStartActivityForResult( + MediaStore.ACTION_IMAGE_CAPTURE_SECURE, REQUEST_CODE_TAKE_PHOTO); + } + + @Test + public void choosePhotoHasCorrectIntentAndResultCode() { + mController.choosePhoto(); + + verifyStartActivityForResult( + MediaStore.ACTION_PICK_IMAGES, REQUEST_CODE_CHOOSE_PHOTO); + } + + @Test + public void takePhotoIsFollowedByCrop() throws IOException { + new File(mImagesDir, "file.txt").createNewFile(); + + Intent intent = new Intent(); + intent.setData(Uri.parse( + "content://com.android.settingslib.test/my_cache/multi_user/file.txt")); + mController.onActivityResult( + REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent); + + verifyStartSystemActivityForResult( + "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO); + } + + @Test + public void takePhotoIsNotFollowedByCropWhenResultCodeNotOk() throws IOException { + new File(mImagesDir, "file.txt").createNewFile(); + + Intent intent = new Intent(); + intent.setData(Uri.parse( + "content://com.android.settingslib.test/my_cache/multi_user/file.txt")); + mController.onActivityResult( + REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_CANCELED, intent); + + verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt()); + verify(mMockAvatarUi, never()).startSystemActivityForResult(any(), anyInt()); + } + + @Test + public void takePhotoIsFollowedByCropWhenTakePhotoUriReturned() throws IOException { + new File(mImagesDir, "TakeEditUserPhoto.jpg").createNewFile(); + + Intent intent = new Intent(); + intent.setData(mTakePhotoUri); + mController.onActivityResult( + REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent); + + verifyStartSystemActivityForResult( + "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO); + } + + @Test + public void takePhotoIsNotFollowedByCropIntentWhenCropNotSupported() throws IOException { + when(mMockAvatarUi.startSystemActivityForResult(any(), anyInt())).thenReturn(false); + + File file = new File(mImagesDir, "file.txt"); + saveBitmapToFile(file); + + Intent intent = new Intent(); + intent.setData(Uri.parse( + "content://com.android.settingslib.test/my_cache/multi_user/file.txt")); + mController.onActivityResult( + REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent); + + verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt()); + verify(mMockAvatarUi, never()).startSystemActivityForResult(any(), anyInt()); + } + + @Test + public void choosePhotoIsFollowedByCrop() throws IOException { + new File(mImagesDir, "file.txt").createNewFile(); + + Intent intent = new Intent(); + intent.setData(Uri.parse( + "content://com.android.settingslib.test/my_cache/multi_user/file.txt")); + mController.onActivityResult( + REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_OK, intent); + + verifyStartSystemActivityForResult( + "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO); + } + + @Test + public void choosePhotoIsNotFollowedByCropWhenResultCodeNotOk() throws IOException { + new File(mImagesDir, "file.txt").createNewFile(); + + Intent intent = new Intent(); + intent.setData(Uri.parse( + "content://com.android.settingslib.test/my_cache/multi_user/file.txt")); + mController.onActivityResult( + REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_CANCELED, intent); + + verify(mMockAvatarUi, never()).startActivityForResult(any(), anyInt()); + verify(mMockAvatarUi, never()).startSystemActivityForResult(any(), anyInt()); + } + + @Test + public void choosePhotoIsFollowedByCropWhenTakePhotoUriReturned() throws IOException { + new File(mImagesDir, "TakeEditUserPhoto.jpg").createNewFile(); + + Intent intent = new Intent(); + intent.setData(mTakePhotoUri); + mController.onActivityResult( + REQUEST_CODE_CHOOSE_PHOTO, Activity.RESULT_OK, intent); + + verifyStartSystemActivityForResult( + "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO); + } + + @Test + public void cropPhotoResultIsReturnedIfResultOkAndContent() { + Intent intent = new Intent(); + intent.setData(mCropPhotoUri); + mController.onActivityResult(REQUEST_CODE_CROP_PHOTO, Activity.RESULT_OK, intent); + verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS)).returnUriResult(mCropPhotoUri); + } + + @Test + public void cropPhotoResultIsNotReturnedIfResultCancel() { + Intent intent = new Intent(); + intent.setData(mCropPhotoUri); + mController.onActivityResult(REQUEST_CODE_CROP_PHOTO, Activity.RESULT_CANCELED, intent); + verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS).times(0)).returnUriResult(mCropPhotoUri); + } + + @Test + public void cropPhotoResultIsNotReturnedIfResultNotContent() { + Intent intent = new Intent(); + intent.setData(Uri.parse("file://test")); + mController.onActivityResult(REQUEST_CODE_CROP_PHOTO, Activity.RESULT_OK, intent); + verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS).times(0)).returnUriResult(mCropPhotoUri); + } + + @Test + public void cropDoesNotUseTakePhotoUri() throws IOException { + new File(mImagesDir, "file.txt").createNewFile(); + + Intent intent = new Intent(); + intent.setData(Uri.parse( + "content://com.android.settingslib.test/my_cache/multi_user/file.txt")); + mController.onActivityResult( + REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent); + + Intent startIntent = verifyStartSystemActivityForResult( + "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO); + assertThat(startIntent.getData()).isNotEqualTo(mTakePhotoUri); + } + + @Test + public void internalCropUsedIfNoSystemCropperFound() throws IOException { + when(mMockAvatarUi.startSystemActivityForResult(any(), anyInt())).thenReturn(false); + + new File(mImagesDir, "file.txt").createNewFile(); + + Intent intent = new Intent(); + intent.setData(Uri.parse( + "content://com.android.settingslib.test/my_cache/multi_user/file.txt")); + mController.onActivityResult( + REQUEST_CODE_TAKE_PHOTO, Activity.RESULT_OK, intent); + + Intent startIntent = verifyStartSystemActivityForResult( + "com.android.camera.action.CROP", REQUEST_CODE_CROP_PHOTO); + assertThat(startIntent.getData()).isNotEqualTo(mTakePhotoUri); + + verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS)).returnUriResult(mCropPhotoUri); + + InputStream imageStream = mContext.getContentResolver().openInputStream(mCropPhotoUri); + Bitmap bitmap = BitmapFactory.decodeStream(imageStream); + assertThat(bitmap.getWidth()).isEqualTo(PHOTO_SIZE); + assertThat(bitmap.getHeight()).isEqualTo(PHOTO_SIZE); + } + + private Intent verifyStartActivityForResult(String action, int resultCode) { + ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); + verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS)) + .startActivityForResult(captor.capture(), eq(resultCode)); + Intent intent = captor.getValue(); + assertThat(intent.getAction()).isEqualTo(action); + return intent; + } + + private Intent verifyStartSystemActivityForResult(String action, int resultCode) { + ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); + verify(mMockAvatarUi, timeout(TIMEOUT_MILLIS)) + .startSystemActivityForResult(captor.capture(), eq(resultCode)); + Intent intent = captor.getValue(); + assertThat(intent.getAction()).isEqualTo(action); + return intent; + } + + private void saveBitmapToFile(File file) throws IOException { + Bitmap bitmap = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888); + OutputStream os = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, os); + os.flush(); + os.close(); + } + +} diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 42aa2053e1a4..fa3360cd31f6 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -231,6 +231,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.SKIP_DIRECTION, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SILENCE_GESTURE, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, JSON_OBJECT_VALIDATOR); + VALIDATORS.put(Secure.NAV_BAR_FORCE_VISIBLE, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.NAV_BAR_KIDS_MODE, BOOLEAN_VALIDATOR); VALIDATORS.put( Secure.NAVIGATION_MODE, new DiscreteValueValidator(new String[] {"0", "1", "2"})); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index a2b699288868..1c5bf81faf3e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2253,8 +2253,11 @@ class SettingsProtoDumpUtil { SecureSettingsProto.MULTI_PRESS_TIMEOUT); dumpSetting(s, p, + Settings.Secure.NAV_BAR_FORCE_VISIBLE, + SecureSettingsProto.NavBar.NAV_BAR_FORCE_VISIBLE); + dumpSetting(s, p, Settings.Secure.NAV_BAR_KIDS_MODE, - SecureSettingsProto.NAV_BAR_KIDS_MODE); + SecureSettingsProto.NavBar.NAV_BAR_KIDS_MODE); dumpSetting(s, p, Settings.Secure.NAVIGATION_MODE, diff --git a/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_fp.xml b/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_fp.xml index 0ae5dc745478..5084ca48e608 100644 --- a/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_fp.xml +++ b/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_fp.xml @@ -1,254 +1 @@ -<?xml version="1.0" encoding="utf-8" ?> -<!-- Copyright (C) 2021 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. ---> -<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:aapt="http://schemas.android.com/aapt"> - <aapt:attr name="android:drawable"> - <vector android:height="60dp" android:width="60dp" android:viewportHeight="60" - android:viewportWidth="60"> - <group android:name="_R_G"> - <group android:name="_R_G_L_1_G" android:translateX="-0.05000000000000071"> - <group android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0" android:translateX="30" - android:translateY="38.75" android:scaleX="1" android:scaleY="1"> - <path android:name="_R_G_L_1_G_D_0_P_0" - android:fillColor="@color/biometric_dialog_error" - android:fillAlpha="1" android:fillType="nonZero" - android:pathData=" M-1.2 -1.25 C-1.2,-1.25 1.2,-1.25 1.2,-1.25 C1.2,-1.25 1.2,1.25 1.2,1.25 C1.2,1.25 -1.2,1.25 -1.2,1.25 C-1.2,1.25 -1.2,-1.25 -1.2,-1.25c "/> - </group> - <group android:name="_R_G_L_1_G_D_1_P_0_G_0_T_0" android:translateX="30" - android:translateY="25" android:pivotX="0.002" android:pivotY="7.488" - android:scaleX="1" android:scaleY="1"> - <path android:name="_R_G_L_1_G_D_1_P_0" - android:fillColor="@color/biometric_dialog_error" - android:fillAlpha="1" android:fillType="nonZero" - android:pathData=" M-1.2 -7.5 C-1.2,-7.5 1.2,-7.5 1.2,-7.5 C1.2,-7.5 1.2,7.5 1.2,7.5 C1.2,7.5 -1.2,7.5 -1.2,7.5 C-1.2,7.5 -1.2,-7.5 -1.2,-7.5c "/> - </group> - <path android:name="_R_G_L_1_G_D_2_P_0" - android:strokeColor="@color/biometric_dialog_error" - android:strokeLineCap="round" android:strokeLineJoin="round" - android:strokeWidth="2.5" android:strokeAlpha="1" - android:trimPathStart="0" android:trimPathEnd="1" - android:trimPathOffset="0" - android:pathData=" M30 6.2 C16.9,6.2 6.3,16.8 6.3,30 C6.3,43.2 16.9,53.8 30,53.8 C43.1,53.8 53.8,43.2 53.8,30 C53.8,16.8 43.1,6.2 30,6.2c "/> - </group> - <group android:name="_R_G_L_0_G" android:translateX="-10.325" - android:translateY="-10.25"> - <path android:name="_R_G_L_0_G_D_0_P_0" - android:strokeColor="@color/biometric_dialog_accent" - android:strokeLineCap="round" android:strokeLineJoin="round" - android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" - android:trimPathEnd="0" android:trimPathOffset="0" - android:pathData=" M31.41 48.43 C30.78,46.69 30.78,44.91 30.78,44.91 C30.78,40.09 34.88,36.16 40.32,36.16 C45.77,36.16 49.87,40.09 49.87,44.91 C49.87,44.91 49.87,45.17 49.87,45.17 C49.87,46.97 48.41,48.43 46.61,48.43 C45.28,48.43 44.09,47.63 43.6,46.39 C43.6,46.39 42.51,43.66 42.51,43.66 C42.02,42.42 40.82,41.61 39.49,41.61 C37.69,41.61 36.23,43.07 36.23,44.87 C36.23,47.12 37.26,49.26 39.02,50.67 C39.02,50.67 39.64,51.16 39.64,51.16 "/> - <path android:name="_R_G_L_0_G_D_1_P_0" - android:strokeColor="@color/biometric_dialog_accent" - android:strokeLineCap="round" android:strokeLineJoin="round" - android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" - android:trimPathEnd="0" android:trimPathOffset="0" - android:pathData=" M32.14 27.3 C34.5,26 37.31,25.25 40.33,25.25 C43.34,25.25 46.15,26 48.51,27.3 "/> - <path android:name="_R_G_L_0_G_D_2_P_0" - android:strokeColor="@color/biometric_dialog_accent" - android:strokeLineCap="round" android:strokeLineJoin="round" - android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" - android:trimPathEnd="0" android:trimPathOffset="0" - android:pathData=" M29.42 36.16 C31.35,32.94 35.51,30.71 40.33,30.71 C45.14,30.71 49.3,32.94 51.23,36.16 "/> - <path android:name="_R_G_L_0_G_D_3_P_0" - android:strokeColor="@color/biometric_dialog_accent" - android:strokeLineCap="round" android:strokeLineJoin="round" - android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" - android:trimPathEnd="0" android:trimPathOffset="0" - android:pathData=" M47.14 52.52 C45.33,54.21 42.94,55.25 40.33,55.25 C37.71,55.25 35.32,54.21 33.51,52.52 "/> - </group> - </group> - <group android:name="time_group"/> - </vector> - </aapt:attr> - <target android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="scaleX" android:duration="67" - android:startOffset="0" android:valueFrom="1" android:valueTo="1.1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.853,0 0.6,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleY" android:duration="67" - android:startOffset="0" android:valueFrom="1" android:valueTo="1.1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.853,0 0.6,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleX" android:duration="100" - android:startOffset="67" android:valueFrom="1.1" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.92,1.06 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleY" android:duration="100" - android:startOffset="67" android:valueFrom="1.1" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.92,1.06 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_1_G_D_1_P_0_G_0_T_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="scaleX" android:duration="67" - android:startOffset="0" android:valueFrom="1" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.659,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleY" android:duration="67" - android:startOffset="0" android:valueFrom="1" android:valueTo="1.1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.6,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleX" android:duration="100" - android:startOffset="67" android:valueFrom="1" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.6,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleY" android:duration="100" - android:startOffset="67" android:valueFrom="1.1" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.92,1.096 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_1_G_D_2_P_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="trimPathEnd" android:duration="67" - android:startOffset="0" android:valueFrom="1" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="trimPathEnd" android:duration="133" - android:startOffset="67" android:valueFrom="1" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_D_0_P_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="trimPathEnd" android:duration="83" - android:startOffset="0" android:valueFrom="0" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="trimPathEnd" android:duration="250" - android:startOffset="83" android:valueFrom="0" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_D_1_P_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="trimPathEnd" android:duration="83" - android:startOffset="0" android:valueFrom="0" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="trimPathEnd" android:duration="250" - android:startOffset="83" android:valueFrom="0" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_D_2_P_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="trimPathEnd" android:duration="83" - android:startOffset="0" android:valueFrom="0" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="trimPathEnd" android:duration="250" - android:startOffset="83" android:valueFrom="0" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_D_3_P_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="trimPathEnd" android:duration="83" - android:startOffset="0" android:valueFrom="0" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="trimPathEnd" android:duration="250" - android:startOffset="83" android:valueFrom="0" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="time_group"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="translateX" android:duration="417" - android:startOffset="0" android:valueFrom="0" android:valueTo="1" - android:valueType="floatType"/> - </set> - </aapt:attr> - </target> -</animated-vector> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="80dp" android:width="80dp" android:viewportHeight="80" android:viewportWidth="80"><group android:name="_R_G"><group android:name="_R_G_L_3_G" android:translateX="-0.25" android:translateY="-0.25"><path android:name="_R_G_L_3_G_D_0_P_0" android:fillColor="#474747" android:fillAlpha="0" android:fillType="nonZero" android:pathData=" M40.21 0.25 C18.13,0.25 0.25,18.17 0.25,40.25 C0.25,62.33 18.13,80.25 40.21,80.25 C62.33,80.25 80.25,62.33 80.25,40.25 C80.25,18.17 62.33,0.25 40.21,0.25c "/></group><group android:name="_R_G_L_2_G" android:translateX="-0.25" android:translateY="-0.25" android:pivotX="40.25" android:pivotY="40.25" android:scaleX="0.975" android:scaleY="0.975"><path android:name="_R_G_L_2_G_D_0_P_0" android:strokeColor="#f2b8b5" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2.5" android:strokeAlpha="1" android:pathData=" M40.21 0.25 C18.13,0.25 0.25,18.17 0.25,40.25 C0.25,62.33 18.13,80.25 40.21,80.25 C62.33,80.25 80.25,62.33 80.25,40.25 C80.25,18.17 62.33,0.25 40.21,0.25c "/></group><group android:name="_R_G_L_1_G" android:translateX="9.950000000000003" android:translateY="10" android:pivotX="30" android:pivotY="30" android:scaleX="1.2" android:scaleY="1.2"><group android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0" android:translateX="30" android:translateY="38.75" android:scaleX="1" android:scaleY="1"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#f2b8b5" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-1.2 -1.25 C-1.2,-1.25 1.2,-1.25 1.2,-1.25 C1.2,-1.25 1.2,1.25 1.2,1.25 C1.2,1.25 -1.2,1.25 -1.2,1.25 C-1.2,1.25 -1.2,-1.25 -1.2,-1.25c "/></group><group android:name="_R_G_L_1_G_D_1_P_0_G_0_T_0" android:translateX="30" android:translateY="25" android:pivotX="0.002" android:pivotY="7.488" android:scaleX="1" android:scaleY="1"><path android:name="_R_G_L_1_G_D_1_P_0" android:fillColor="#f2b8b5" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-1.2 -7.5 C-1.2,-7.5 1.2,-7.5 1.2,-7.5 C1.2,-7.5 1.2,7.5 1.2,7.5 C1.2,7.5 -1.2,7.5 -1.2,7.5 C-1.2,7.5 -1.2,-7.5 -1.2,-7.5c "/></group></group><group android:name="_R_G_L_0_G" android:translateX="20.659" android:translateY="15.75"><path android:name="_R_G_L_0_G_D_0_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M27.52 38.98 C26.49,39.95 25.29,40.73 23.98,41.29 C23.17,41.65 22.31,41.91 21.41,42.07 C20.74,42.19 20.05,42.25 19.34,42.25 C18.44,42.25 17.56,42.15 16.72,41.96 C15.93,41.77 15.16,41.51 14.43,41.18 C13.23,40.63 12.13,39.88 11.16,38.98 "/><path android:name="_R_G_L_0_G_D_1_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M8.64 34.07 C7.89,31.97 7.89,29.85 7.89,29.85 C7.89,24.05 12.81,19.34 19.34,19.34 C25.87,19.34 30.8,24.05 30.8,29.85 C30.8,29.85 30.8,30.16 30.8,30.16 C30.8,32.32 29.04,34.07 26.89,34.07 C25.28,34.07 23.86,33.1 23.27,31.61 C23.27,31.61 21.96,28.34 21.96,28.34 C21.37,26.85 19.93,25.89 18.34,25.89 C16.18,25.89 14.43,27.64 14.43,29.8 C14.43,31.42 14.87,32.99 15.68,34.36 C16.22,35.26 16.93,36.08 17.77,36.75 C17.77,36.75 18.52,37.34 18.52,37.34 "/><path android:name="_R_G_L_0_G_D_2_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M6.25 19.34 C7.48,17.3 9.46,15.58 11.9,14.42 C12.93,13.94 14.03,13.55 15.2,13.27 C16.51,12.96 17.9,12.8 19.34,12.8 C20.77,12.8 22.14,12.96 23.45,13.26 C24.9,13.6 26.26,14.12 27.48,14.78 C29.6,15.92 31.32,17.5 32.43,19.34 "/><path android:name="_R_G_L_0_G_D_3_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" android:pathData=" M9.52 8.7 C10.98,7.91 12.58,7.28 14.28,6.86 C15.89,6.46 17.58,6.25 19.34,6.25 C21.06,6.25 22.72,6.45 24.3,6.83 C26.04,7.25 27.67,7.89 29.16,8.7 "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_3_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="fillAlpha" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="fillAlpha" android:duration="167" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_2_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="strokeWidth" android:duration="167" android:startOffset="0" android:valueFrom="2.5" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_2_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="strokeAlpha" android:duration="83" android:startOffset="0" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="strokeAlpha" android:duration="83" android:startOffset="83" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_2_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="0.975" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_2_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="167" android:valueFrom="0.975" android:valueTo="0" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="0" android:valueFrom="1" android:valueTo="1.1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.853,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="0" android:valueFrom="1" android:valueTo="1.1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.853,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="100" android:startOffset="67" android:valueFrom="1.1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.92,1.06 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="100" android:startOffset="67" android:valueFrom="1.1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.92,1.06 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_1_P_0_G_0_T_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="0" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.659,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="0" android:valueFrom="1" android:valueTo="1.1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="100" android:startOffset="67" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="100" android:startOffset="67" android:valueFrom="1.1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.92,1.096 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="83" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.543,0 0.299,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="250" android:startOffset="83" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.543,0 0.299,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_1_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="83" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.543,0 0.299,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="250" android:startOffset="83" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.543,0 0.299,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_2_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="83" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.543,0 0.299,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="250" android:startOffset="83" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.543,0 0.299,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_3_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathEnd" android:duration="83" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.543,0 0.299,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="trimPathEnd" android:duration="250" android:startOffset="83" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.543,0 0.299,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="417" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_unlock.xml b/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_unlock.xml new file mode 100644 index 000000000000..c4f818146011 --- /dev/null +++ b/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_unlock.xml @@ -0,0 +1 @@ +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="80dp" android:width="80dp" android:viewportHeight="80" android:viewportWidth="80"><group android:name="_R_G"><group android:name="_R_G_L_3_G" android:translateX="-0.25" android:translateY="-0.25"><path android:name="_R_G_L_3_G_D_0_P_0" android:fillColor="#474747" android:fillAlpha="0" android:fillType="nonZero" android:pathData=" M40.21 0.25 C18.13,0.25 0.25,18.17 0.25,40.25 C0.25,62.33 18.13,80.25 40.21,80.25 C62.33,80.25 80.25,62.33 80.25,40.25 C80.25,18.17 62.33,0.25 40.21,0.25c "/></group><group android:name="_R_G_L_2_G" android:translateX="-0.25" android:translateY="-0.25" android:pivotX="40.25" android:pivotY="40.25" android:scaleX="0.975" android:scaleY="0.975"><path android:name="_R_G_L_2_G_D_0_P_0" android:strokeColor="#f2b8b5" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2.5" android:strokeAlpha="1" android:pathData=" M40.21 0.25 C18.13,0.25 0.25,18.17 0.25,40.25 C0.25,62.33 18.13,80.25 40.21,80.25 C62.33,80.25 80.25,62.33 80.25,40.25 C80.25,18.17 62.33,0.25 40.21,0.25c "/></group><group android:name="_R_G_L_1_G" android:translateX="9.950000000000003" android:translateY="10" android:pivotX="30" android:pivotY="30" android:scaleX="1.2" android:scaleY="1.2"><group android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0" android:translateX="30" android:translateY="38.75" android:scaleX="1" android:scaleY="1"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#f2b8b5" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-1.2 -1.25 C-1.2,-1.25 1.2,-1.25 1.2,-1.25 C1.2,-1.25 1.2,1.25 1.2,1.25 C1.2,1.25 -1.2,1.25 -1.2,1.25 C-1.2,1.25 -1.2,-1.25 -1.2,-1.25c "/></group><group android:name="_R_G_L_1_G_D_1_P_0_G_0_T_0" android:translateX="30" android:translateY="25" android:pivotX="0.002" android:pivotY="7.488" android:scaleX="1" android:scaleY="1"><path android:name="_R_G_L_1_G_D_1_P_0" android:fillColor="#f2b8b5" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-1.2 -7.5 C-1.2,-7.5 1.2,-7.5 1.2,-7.5 C1.2,-7.5 1.2,7.5 1.2,7.5 C1.2,7.5 -1.2,7.5 -1.2,7.5 C-1.2,7.5 -1.2,-7.5 -1.2,-7.5c "/></group></group><group android:name="_R_G_L_0_G" android:translateX="20.75" android:translateY="15.75" android:pivotX="19.341" android:pivotY="24.25" android:scaleX="0.5" android:scaleY="0"><path android:name="_R_G_L_0_G_D_0_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M30.64 30.14 C30.64,30.14 30.64,38.14 30.64,38.14 C30.64,38.77 30.36,39.32 29.91,39.69 C29.57,39.97 29.12,40.14 28.64,40.14 C28.64,40.14 10.14,40.14 10.14,40.14 C9.04,40.14 8.14,39.25 8.14,38.14 C8.14,38.14 8.14,30.14 8.14,30.14 "/><path android:name="_R_G_L_0_G_D_1_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M19.42 31.53 C18.15,31.52 18.11,30.33 18.11,30.33 C18.11,29.59 18.66,28.98 19.4,28.98 C20.13,28.98 20.69,29.59 20.69,30.33 C20.69,30.33 20.69,30.37 20.69,30.37 C20.69,30.64 20.49,30.87 20.25,30.87 C20.07,30.87 19.91,30.74 19.84,30.55 C19.84,30.55 19.69,30.14 19.69,30.14 C19.63,29.94 19.46,29.82 19.28,29.82 C19.04,29.82 18.61,30.02 18.61,30.29 C18.61,30.43 18.6,30.75 18.76,31.03 C18.87,31.21 19.21,31.77 19.96,31.41 C20.69,31.01 20.69,30.34 20.69,30.34 "/><path android:name="_R_G_L_0_G_D_2_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M8.14 30.22 C8.14,30.22 8.14,22.22 8.14,22.22 C8.14,21.71 8.33,21.25 8.64,20.9 C9,20.48 9.54,20.22 10.14,20.22 C10.14,20.22 28.64,20.22 28.64,20.22 C29.75,20.22 30.64,21.11 30.64,22.22 C30.64,22.22 30.64,30.14 30.64,30.14 "/><path android:name="_R_G_L_0_G_D_3_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_3_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="fillAlpha" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="fillAlpha" android:duration="167" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_2_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="strokeWidth" android:duration="167" android:startOffset="0" android:valueFrom="2.5" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_2_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="strokeAlpha" android:duration="83" android:startOffset="0" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="strokeAlpha" android:duration="83" android:startOffset="83" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_2_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="0" android:valueFrom="0" android:valueTo="0.975" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_2_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="167" android:valueFrom="0.975" android:valueTo="0" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="0" android:valueFrom="1" android:valueTo="1.1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.853,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="0" android:valueFrom="1" android:valueTo="1.1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.853,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="100" android:startOffset="67" android:valueFrom="1.1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.92,1.06 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="100" android:startOffset="67" android:valueFrom="1.1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.92,1.06 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_1_P_0_G_0_T_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="0" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.659,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="0" android:valueFrom="1" android:valueTo="1.1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="100" android:startOffset="67" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.6,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="100" android:startOffset="67" android:valueFrom="1.1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.8,0 0.92,1.096 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_3_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="233" android:startOffset="0" android:valueFrom="M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 " android:valueTo="M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="267" android:startOffset="233" android:valueFrom="M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 " android:valueTo="M37.91 20.05 C37.91,20.05 37.89,14.16 37.89,14.16 C37.89,10.79 35.15,8.05 31.86,8.03 C28.46,8.01 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="167" android:startOffset="0" android:valueFrom="0.5" android:valueTo="0.5" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.05,1.4 0.1,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="167" android:startOffset="0" android:valueFrom="0.5" android:valueTo="0.5" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.05,1.4 0.1,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="333" android:startOffset="167" android:valueFrom="0.5" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.05,1.4 0.1,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="333" android:startOffset="167" android:valueFrom="0.5" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.05,1.4 0.1,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="167" android:valueFrom="0" android:valueTo="0.5" android:valueType="floatType"/></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="683" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_error.xml b/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_error.xml index fc2c7d00f3a7..c05a8d55c16c 100644 --- a/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_error.xml +++ b/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_error.xml @@ -1,247 +1 @@ -<?xml version="1.0" encoding="utf-8" ?> -<!-- Copyright (C) 2021 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. ---> -<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:aapt="http://schemas.android.com/aapt"> - <aapt:attr name="android:drawable"> - <vector android:height="60dp" android:width="60dp" android:viewportHeight="60" - android:viewportWidth="60"> - <group android:name="_R_G"> - <group android:name="_R_G_L_1_G" android:translateX="-0.05000000000000071"> - <group android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0" android:translateX="30" - android:translateY="38.75" android:scaleX="0" android:scaleY="0"> - <path android:name="_R_G_L_1_G_D_0_P_0" - android:fillColor="@color/biometric_dialog_error" - android:fillAlpha="1" android:fillType="nonZero" - android:pathData=" M-1.2 -1.25 C-1.2,-1.25 1.2,-1.25 1.2,-1.25 C1.2,-1.25 1.2,1.25 1.2,1.25 C1.2,1.25 -1.2,1.25 -1.2,1.25 C-1.2,1.25 -1.2,-1.25 -1.2,-1.25c "/> - </group> - <group android:name="_R_G_L_1_G_D_1_P_0_G_0_T_0" android:translateX="30" - android:translateY="25" android:pivotX="0.002" android:pivotY="7.488" - android:scaleX="1" android:scaleY="0"> - <path android:name="_R_G_L_1_G_D_1_P_0" - android:fillColor="@color/biometric_dialog_error" - android:fillAlpha="1" android:fillType="nonZero" - android:pathData=" M-1.2 -7.5 C-1.2,-7.5 1.2,-7.5 1.2,-7.5 C1.2,-7.5 1.2,7.5 1.2,7.5 C1.2,7.5 -1.2,7.5 -1.2,7.5 C-1.2,7.5 -1.2,-7.5 -1.2,-7.5c "/> - </group> - <path android:name="_R_G_L_1_G_D_2_P_0" - android:strokeColor="@color/biometric_dialog_error" - android:strokeLineCap="round" android:strokeLineJoin="round" - android:strokeWidth="2.5" android:strokeAlpha="1" - android:trimPathStart="1" android:trimPathEnd="1" - android:trimPathOffset="0" - android:pathData=" M30 6.2 C16.9,6.2 6.3,16.8 6.3,30 C6.3,43.2 16.9,53.8 30,53.8 C43.1,53.8 53.8,43.2 53.8,30 C53.8,16.8 43.1,6.2 30,6.2c "/> - </group> - <group android:name="_R_G_L_0_G" android:translateX="-10.325" - android:translateY="-10.25"> - <path android:name="_R_G_L_0_G_D_0_P_0" - android:strokeColor="@color/biometric_dialog_accent" - android:strokeLineCap="round" android:strokeLineJoin="round" - android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" - android:trimPathEnd="1" android:trimPathOffset="0" - android:pathData=" M31.41 48.43 C30.78,46.69 30.78,44.91 30.78,44.91 C30.78,40.09 34.88,36.16 40.32,36.16 C45.77,36.16 49.87,40.09 49.87,44.91 C49.87,44.91 49.87,45.17 49.87,45.17 C49.87,46.97 48.41,48.43 46.61,48.43 C45.28,48.43 44.09,47.63 43.6,46.39 C43.6,46.39 42.51,43.66 42.51,43.66 C42.02,42.42 40.82,41.61 39.49,41.61 C37.69,41.61 36.23,43.07 36.23,44.87 C36.23,47.12 37.26,49.26 39.02,50.67 C39.02,50.67 39.64,51.16 39.64,51.16 "/> - <path android:name="_R_G_L_0_G_D_1_P_0" - android:strokeColor="@color/biometric_dialog_accent" - android:strokeLineCap="round" android:strokeLineJoin="round" - android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" - android:trimPathEnd="1" android:trimPathOffset="0" - android:pathData=" M32.14 27.3 C34.5,26 37.31,25.25 40.33,25.25 C43.34,25.25 46.15,26 48.51,27.3 "/> - <path android:name="_R_G_L_0_G_D_2_P_0" - android:strokeColor="@color/biometric_dialog_accent" - android:strokeLineCap="round" android:strokeLineJoin="round" - android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" - android:trimPathEnd="1" android:trimPathOffset="0" - android:pathData=" M29.42 36.16 C31.35,32.94 35.51,30.71 40.33,30.71 C45.14,30.71 49.3,32.94 51.23,36.16 "/> - <path android:name="_R_G_L_0_G_D_3_P_0" - android:strokeColor="@color/biometric_dialog_accent" - android:strokeLineCap="round" android:strokeLineJoin="round" - android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" - android:trimPathEnd="1" android:trimPathOffset="0" - android:pathData=" M47.14 52.52 C45.33,54.21 42.94,55.25 40.33,55.25 C37.71,55.25 35.32,54.21 33.51,52.52 "/> - </group> - </group> - <group android:name="time_group"/> - </vector> - </aapt:attr> - <target android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="scaleX" android:duration="167" - android:startOffset="0" android:valueFrom="0" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.06 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleY" android:duration="167" - android:startOffset="0" android:valueFrom="0" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.06 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleX" android:duration="100" - android:startOffset="167" android:valueFrom="0" - android:valueTo="1.1" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.06 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleY" android:duration="100" - android:startOffset="167" android:valueFrom="0" - android:valueTo="1.1" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.06 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleX" android:duration="67" - android:startOffset="267" android:valueFrom="1.1" - android:valueTo="1" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.147,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleY" android:duration="67" - android:startOffset="267" android:valueFrom="1.1" - android:valueTo="1" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.147,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_1_G_D_1_P_0_G_0_T_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="scaleX" android:duration="167" - android:startOffset="0" android:valueFrom="1" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleY" android:duration="167" - android:startOffset="0" android:valueFrom="0" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.096 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleX" android:duration="100" - android:startOffset="167" android:valueFrom="1" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleY" android:duration="100" - android:startOffset="167" android:valueFrom="0" - android:valueTo="1.1" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.096 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleX" android:duration="67" - android:startOffset="267" android:valueFrom="1" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.341,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - <objectAnimator android:propertyName="scaleY" android:duration="67" - android:startOffset="267" android:valueFrom="1.1" - android:valueTo="1" android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_1_G_D_2_P_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="trimPathStart" android:duration="267" - android:startOffset="0" android:valueFrom="1" android:valueTo="0" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_D_0_P_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="trimPathStart" android:duration="167" - android:startOffset="0" android:valueFrom="0" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_D_1_P_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="trimPathStart" android:duration="167" - android:startOffset="0" android:valueFrom="0" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_D_2_P_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="trimPathStart" android:duration="167" - android:startOffset="0" android:valueFrom="0" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="_R_G_L_0_G_D_3_P_0"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="trimPathStart" android:duration="167" - android:startOffset="0" android:valueFrom="0" android:valueTo="1" - android:valueType="floatType"> - <aapt:attr name="android:interpolator"> - <pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/> - </aapt:attr> - </objectAnimator> - </set> - </aapt:attr> - </target> - <target android:name="time_group"> - <aapt:attr name="android:animation"> - <set android:ordering="together"> - <objectAnimator android:propertyName="translateX" android:duration="350" - android:startOffset="0" android:valueFrom="0" android:valueTo="1" - android:valueType="floatType"/> - </set> - </aapt:attr> - </target> -</animated-vector> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="80dp" android:width="80dp" android:viewportHeight="80" android:viewportWidth="80"><group android:name="_R_G"><group android:name="_R_G_L_3_G" android:translateX="-0.25" android:translateY="-0.25"><path android:name="_R_G_L_3_G_D_0_P_0" android:fillColor="#474747" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M40.21 0.25 C18.13,0.25 0.25,18.17 0.25,40.25 C0.25,62.33 18.13,80.25 40.21,80.25 C62.33,80.25 80.25,62.33 80.25,40.25 C80.25,18.17 62.33,0.25 40.21,0.25c "/></group><group android:name="_R_G_L_2_G" android:translateX="-0.25" android:translateY="-0.25" android:pivotX="40.25" android:pivotY="40.25" android:scaleX="0.975" android:scaleY="0"><path android:name="_R_G_L_2_G_D_0_P_0" android:strokeColor="#f2b8b5" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0" android:strokeAlpha="0" android:pathData=" M40.21 0.25 C18.13,0.25 0.25,18.17 0.25,40.25 C0.25,62.33 18.13,80.25 40.21,80.25 C62.33,80.25 80.25,62.33 80.25,40.25 C80.25,18.17 62.33,0.25 40.21,0.25c "/></group><group android:name="_R_G_L_1_G" android:translateX="9.950000000000003" android:translateY="10" android:pivotX="30" android:pivotY="30" android:scaleX="1.2" android:scaleY="1.2"><group android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0" android:translateX="30" android:translateY="38.75" android:scaleX="0" android:scaleY="0"><path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#f2b8b5" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-1.2 -1.25 C-1.2,-1.25 1.2,-1.25 1.2,-1.25 C1.2,-1.25 1.2,1.25 1.2,1.25 C1.2,1.25 -1.2,1.25 -1.2,1.25 C-1.2,1.25 -1.2,-1.25 -1.2,-1.25c "/></group><group android:name="_R_G_L_1_G_D_1_P_0_G_0_T_0" android:translateX="30" android:translateY="25" android:pivotX="0.002" android:pivotY="7.488" android:scaleX="1" android:scaleY="0"><path android:name="_R_G_L_1_G_D_1_P_0" android:fillColor="#f2b8b5" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-1.2 -7.5 C-1.2,-7.5 1.2,-7.5 1.2,-7.5 C1.2,-7.5 1.2,7.5 1.2,7.5 C1.2,7.5 -1.2,7.5 -1.2,7.5 C-1.2,7.5 -1.2,-7.5 -1.2,-7.5c "/></group></group><group android:name="_R_G_L_0_G" android:translateX="20.659" android:translateY="15.75"><path android:name="_R_G_L_0_G_D_0_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" android:pathData=" M27.52 38.98 C26.49,39.95 25.29,40.73 23.98,41.29 C23.17,41.65 22.31,41.91 21.41,42.07 C20.74,42.19 20.05,42.25 19.34,42.25 C18.44,42.25 17.56,42.15 16.72,41.96 C15.93,41.77 15.16,41.51 14.43,41.18 C13.23,40.63 12.13,39.88 11.16,38.98 "/><path android:name="_R_G_L_0_G_D_1_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" android:pathData=" M8.64 34.07 C7.89,31.97 7.89,29.85 7.89,29.85 C7.89,24.05 12.81,19.34 19.34,19.34 C25.87,19.34 30.8,24.05 30.8,29.85 C30.8,29.85 30.8,30.16 30.8,30.16 C30.8,32.32 29.04,34.07 26.89,34.07 C25.28,34.07 23.86,33.1 23.27,31.61 C23.27,31.61 21.96,28.34 21.96,28.34 C21.37,26.85 19.93,25.89 18.34,25.89 C16.18,25.89 14.43,27.64 14.43,29.8 C14.43,31.42 14.87,32.99 15.68,34.36 C16.22,35.26 16.93,36.08 17.77,36.75 C17.77,36.75 18.52,37.34 18.52,37.34 "/><path android:name="_R_G_L_0_G_D_2_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" android:pathData=" M6.25 19.34 C7.48,17.3 9.46,15.58 11.9,14.42 C12.93,13.94 14.03,13.55 15.2,13.27 C16.51,12.96 17.9,12.8 19.34,12.8 C20.77,12.8 22.14,12.96 23.45,13.26 C24.9,13.6 26.26,14.12 27.48,14.78 C29.6,15.92 31.32,17.5 32.43,19.34 "/><path android:name="_R_G_L_0_G_D_3_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" android:pathData=" M9.52 8.7 C10.98,7.91 12.58,7.28 14.28,6.86 C15.89,6.46 17.58,6.25 19.34,6.25 C21.06,6.25 22.72,6.45 24.3,6.83 C26.04,7.25 27.67,7.89 29.16,8.7 "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_3_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="fillAlpha" android:duration="83" android:startOffset="0" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_2_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="strokeWidth" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="strokeWidth" android:duration="233" android:startOffset="67" android:valueFrom="0" android:valueTo="2.5" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_2_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="strokeAlpha" android:duration="67" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="strokeAlpha" android:duration="83" android:startOffset="67" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_2_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleY" android:duration="0" android:startOffset="67" android:valueFrom="0" android:valueTo="0.975" android:valueType="floatType"/></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_0_P_0_G_0_T_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="167" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.06 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="167" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.06 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="100" android:startOffset="167" android:valueFrom="0" android:valueTo="1.1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.06 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="100" android:startOffset="167" android:valueFrom="0" android:valueTo="1.1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.06 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="267" android:valueFrom="1.1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.147,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="267" android:valueFrom="1.1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.147,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_1_P_0_G_0_T_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="167" android:startOffset="0" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="167" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.096 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="100" android:startOffset="167" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="100" android:startOffset="167" android:valueFrom="0" android:valueTo="1.1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.08,0.096 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="67" android:startOffset="267" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.341,0 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="67" android:startOffset="267" android:valueFrom="1.1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.4,0 0.2,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathStart" android:duration="167" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_1_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathStart" android:duration="167" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_2_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathStart" android:duration="167" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_3_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="trimPathStart" android:duration="167" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="350" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_unlock.xml b/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_unlock.xml new file mode 100644 index 000000000000..16944294a94e --- /dev/null +++ b/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_unlock.xml @@ -0,0 +1 @@ +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="80dp" android:width="80dp" android:viewportHeight="80" android:viewportWidth="80"><group android:name="_R_G"><group android:name="_R_G_L_2_G" android:translateX="-0.25" android:translateY="-0.25"><path android:name="_R_G_L_2_G_D_0_P_0" android:fillColor="#474747" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M40.21 0.25 C18.13,0.25 0.25,18.17 0.25,40.25 C0.25,62.33 18.13,80.25 40.21,80.25 C62.33,80.25 80.25,62.33 80.25,40.25 C80.25,18.17 62.33,0.25 40.21,0.25c "/></group><group android:name="_R_G_L_1_G" android:translateX="20.75" android:translateY="15.75"><path android:name="_R_G_L_1_G_D_0_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M27.52 38.98 C26.49,39.95 25.29,40.73 23.98,41.29 C23.17,41.65 22.31,41.91 21.41,42.07 C20.74,42.19 20.05,42.25 19.34,42.25 C18.44,42.25 17.56,42.15 16.72,41.96 C15.93,41.77 15.16,41.51 14.43,41.18 C13.23,40.63 12.13,39.88 11.16,38.98 "/><path android:name="_R_G_L_1_G_D_1_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M8.64 34.07 C7.89,31.97 7.89,29.85 7.89,29.85 C7.89,24.05 12.81,19.34 19.34,19.34 C25.87,19.34 30.8,24.05 30.8,29.85 C30.8,29.85 30.8,30.16 30.8,30.16 C30.8,32.32 29.04,34.07 26.89,34.07 C25.28,34.07 23.86,33.1 23.27,31.61 C23.27,31.61 21.96,28.34 21.96,28.34 C21.37,26.85 19.93,25.89 18.34,25.89 C16.18,25.89 14.43,27.64 14.43,29.8 C14.43,31.42 14.87,32.99 15.68,34.36 C16.22,35.26 16.93,36.08 17.77,36.75 C17.77,36.75 18.52,37.34 18.52,37.34 "/><path android:name="_R_G_L_1_G_D_2_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M6.25 19.34 C7.48,17.3 9.46,15.58 11.9,14.42 C12.93,13.94 14.03,13.55 15.2,13.27 C16.51,12.96 17.9,12.8 19.34,12.8 C20.77,12.8 22.14,12.96 23.45,13.26 C24.9,13.6 26.26,14.12 27.48,14.78 C29.6,15.92 31.32,17.5 32.43,19.34 "/><path android:name="_R_G_L_1_G_D_3_P_0" android:strokeColor="#d3e3fd" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M9.52 8.7 C10.98,7.91 12.58,7.28 14.28,6.86 C15.89,6.46 17.58,6.25 19.34,6.25 C21.06,6.25 22.72,6.45 24.3,6.83 C26.04,7.25 27.67,7.89 29.16,8.7 "/></group><group android:name="_R_G_L_0_G" android:translateX="37.357" android:translateY="43.25" android:pivotX="2.75" android:pivotY="2.75" android:scaleX="1.41866" android:scaleY="1.41866"><path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#d3e3fd" android:fillAlpha="0" android:fillType="nonZero" android:pathData=" M2.75 5.25 C4.13,5.25 5.25,4.13 5.25,2.75 C5.25,1.37 4.13,0.25 2.75,0.25 C1.37,0.25 0.25,1.37 0.25,2.75 C0.25,4.13 1.37,5.25 2.75,5.25c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="_R_G_L_1_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="107" android:startOffset="0" android:valueFrom="M27.52 38.98 C26.49,39.95 25.29,40.73 23.98,41.29 C23.17,41.65 22.31,41.91 21.41,42.07 C20.74,42.19 20.05,42.25 19.34,42.25 C18.44,42.25 17.56,42.15 16.72,41.96 C15.93,41.77 15.16,41.51 14.43,41.18 C13.23,40.63 12.13,39.88 11.16,38.98 " android:valueTo="M30.81 32.26 C30.56,32.49 30.27,38.76 29.96,38.9 C29.77,39.46 29.13,39.94 28.57,40.26 C28.15,40.51 26.93,40.65 26.4,40.65 C26.18,40.65 11.91,40.62 11.71,40.58 C10.68,40.53 9.06,39.79 8.89,38.88 C8.6,38.74 8.34,32.48 8.1,32.27 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.541,0 0.833,0.767 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="143" android:startOffset="107" android:valueFrom="M30.81 32.26 C30.56,32.49 30.27,38.76 29.96,38.9 C29.77,39.46 29.13,39.94 28.57,40.26 C28.15,40.51 26.93,40.65 26.4,40.65 C26.18,40.65 11.91,40.62 11.71,40.58 C10.68,40.53 9.06,39.79 8.89,38.88 C8.6,38.74 8.34,32.48 8.1,32.27 " android:valueTo="M30.64 30.14 C30.64,30.14 30.64,38.14 30.64,38.14 C30.64,38.77 30.36,39.32 29.91,39.69 C29.57,39.97 29.12,40.14 28.64,40.14 C28.64,40.14 10.14,40.14 10.14,40.14 C9.04,40.14 8.14,39.25 8.14,38.14 C8.14,38.14 8.14,30.14 8.14,30.14 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.233 0.331,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_1_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="strokeAlpha" android:duration="140" android:startOffset="0" android:valueFrom="1" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="strokeAlpha" android:duration="50" android:startOffset="140" android:valueFrom="1" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_1_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="107" android:startOffset="0" android:valueFrom="M8.64 34.07 C7.89,31.97 7.89,29.85 7.89,29.85 C7.89,24.05 12.81,19.34 19.34,19.34 C25.87,19.34 30.8,24.05 30.8,29.85 C30.8,29.85 30.8,30.16 30.8,30.16 C30.8,32.32 29.04,34.07 26.89,34.07 C25.28,34.07 23.86,33.1 23.27,31.61 C23.27,31.61 21.96,28.34 21.96,28.34 C21.37,26.85 19.93,25.89 18.34,25.89 C16.18,25.89 14.43,27.64 14.43,29.8 C14.43,31.42 14.87,32.99 15.68,34.36 C16.22,35.26 16.93,36.08 17.77,36.75 C17.77,36.75 18.52,37.34 18.52,37.34 " android:valueTo="M18.93 32.18 C17.11,32.68 16.62,30.26 16.62,30.26 C16.62,28.78 17.81,27.59 19.39,27.59 C20.96,27.59 22.15,28.78 22.15,30.26 C22.15,30.26 22.15,30.34 22.15,30.34 C22.15,30.89 21.11,32.54 19.57,32.19 C19.19,32.1 20.48,31.09 20.34,30.71 C20.34,30.71 20.02,29.88 20.02,29.88 C19.88,29.5 19.53,29.25 19.15,29.25 C18.63,29.25 18,29.67 18,30.22 C18,30.57 18.06,31.08 18.32,31.51 C18.49,31.8 19.02,32.25 19.79,32.04 C20.41,31.7 20.38,31.36 20.38,31.36 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.541,0 0.833,0.767 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="107" android:startOffset="107" android:valueFrom="M18.93 32.18 C17.11,32.68 16.62,30.26 16.62,30.26 C16.62,28.78 17.81,27.59 19.39,27.59 C20.96,27.59 22.15,28.78 22.15,30.26 C22.15,30.26 22.15,30.34 22.15,30.34 C22.15,30.89 21.11,32.54 19.57,32.19 C19.19,32.1 20.48,31.09 20.34,30.71 C20.34,30.71 20.02,29.88 20.02,29.88 C19.88,29.5 19.53,29.25 19.15,29.25 C18.63,29.25 18,29.67 18,30.22 C18,30.57 18.06,31.08 18.32,31.51 C18.49,31.8 19.02,32.25 19.79,32.04 C20.41,31.7 20.38,31.36 20.38,31.36 " android:valueTo="M19.42 31.53 C18.15,31.52 18.11,30.33 18.11,30.33 C18.11,29.59 18.66,28.98 19.4,28.98 C20.13,28.98 20.69,29.59 20.69,30.33 C20.69,30.33 20.69,30.37 20.69,30.37 C20.69,30.64 20.49,30.87 20.25,30.87 C20.07,30.87 19.91,30.74 19.84,30.55 C19.84,30.55 19.69,30.14 19.69,30.14 C19.63,29.94 19.46,29.82 19.28,29.82 C19.04,29.82 18.61,30.02 18.61,30.29 C18.61,30.43 18.6,30.75 18.76,31.03 C18.87,31.21 19.21,31.77 19.96,31.41 C20.69,31.01 20.69,30.34 20.69,30.34 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.233 0,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_2_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="250" android:startOffset="0" android:valueFrom="M6.25 19.34 C7.48,17.3 9.46,15.58 11.9,14.42 C12.93,13.94 14.03,13.55 15.2,13.27 C16.51,12.96 17.9,12.8 19.34,12.8 C20.77,12.8 22.14,12.96 23.45,13.26 C24.9,13.6 26.26,14.12 27.48,14.78 C29.6,15.92 31.32,17.5 32.43,19.34 " android:valueTo="M8.14 30.22 C8.14,30.22 8.14,22.22 8.14,22.22 C8.14,21.71 8.33,21.25 8.64,20.9 C9,20.48 9.54,20.22 10.14,20.22 C10.14,20.22 28.64,20.22 28.64,20.22 C29.75,20.22 30.64,21.11 30.64,22.22 C30.64,22.22 30.64,30.14 30.64,30.14 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.541,0 0.189,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_1_G_D_3_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="pathData" android:duration="95" android:startOffset="0" android:valueFrom="M9.52 8.7 C10.98,7.91 12.58,7.28 14.28,6.86 C15.89,6.46 17.58,6.25 19.34,6.25 C21.06,6.25 22.72,6.45 24.3,6.83 C26.04,7.25 27.67,7.89 29.16,8.7 " android:valueTo="M11.47 14.84 C11.47,14.84 12.21,11.43 13.54,9.84 C14.84,8.28 16.68,7.22 19.35,7.22 C22.01,7.22 23.98,8.4 25.19,10.18 C26.39,11.96 27.25,14.84 27.25,14.84 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.541,0 0.833,0.767 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="24" android:startOffset="95" android:valueFrom="M11.47 14.84 C11.47,14.84 12.21,11.43 13.54,9.84 C14.84,8.28 16.68,7.22 19.35,7.22 C22.01,7.22 23.98,8.4 25.19,10.18 C26.39,11.96 27.25,14.84 27.25,14.84 " android:valueTo="M12.11 16.85 C12.11,16.85 12.82,12.71 13.37,11.5 C14.17,9.24 16.38,7.53 19.35,7.53 C22.32,7.53 24.61,9.32 25.35,11.72 C25.61,12.64 26.62,16.85 26.62,16.85 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.233 0.833,0.767 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="81" android:startOffset="119" android:valueFrom="M12.11 16.85 C12.11,16.85 12.82,12.71 13.37,11.5 C14.17,9.24 16.38,7.53 19.35,7.53 C22.32,7.53 24.61,9.32 25.35,11.72 C25.61,12.64 26.62,16.85 26.62,16.85 " android:valueTo="M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.233 0.261,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="pathData" android:duration="233" android:startOffset="200" android:valueFrom="M13.12 20.04 C13.12,20.04 13.11,14.15 13.11,14.15 C13.11,10.77 15.91,8.04 19.36,8.04 C22.81,8.04 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 " android:valueTo="M37.91 20.05 C37.91,20.05 37.89,14.16 37.89,14.16 C37.89,10.79 35.15,8.05 31.86,8.03 C28.46,8.01 25.61,10.77 25.61,14.15 C25.61,14.15 25.62,20.04 25.62,20.04 " android:valueType="pathType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.123,0 0.23,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G_D_0_P_0"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="fillAlpha" android:duration="120" android:startOffset="0" android:valueFrom="0" android:valueTo="0" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="fillAlpha" android:duration="20" android:startOffset="120" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="_R_G_L_0_G"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="scaleX" android:duration="120" android:startOffset="0" android:valueFrom="1.4186600000000003" android:valueTo="1.4186600000000003" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.43,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="120" android:startOffset="0" android:valueFrom="1.4186600000000003" android:valueTo="1.4186600000000003" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.43,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleX" android:duration="130" android:startOffset="120" android:valueFrom="1.4186600000000003" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.43,1 1.0,1.0"/></aapt:attr></objectAnimator><objectAnimator android:propertyName="scaleY" android:duration="130" android:startOffset="120" android:valueFrom="1.4186600000000003" android:valueTo="1" android:valueType="floatType"><aapt:attr name="android:interpolator"><pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.43,1 1.0,1.0"/></aapt:attr></objectAnimator></set></aapt:attr></target><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="517" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml index 89690e8ff0ec..58adb9146bd0 100644 --- a/packages/SystemUI/res/layout/auth_biometric_contents.xml +++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml @@ -49,8 +49,8 @@ <ImageView android:id="@+id/biometric_icon" - android:layout_width="@dimen/biometric_dialog_biometric_icon_size" - android:layout_height="@dimen/biometric_dialog_biometric_icon_size" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:layout_gravity="center" android:contentDescription="@null" android:scaleType="fitXY" /> diff --git a/packages/SystemUI/res/layout/auth_biometric_face_to_fingerprint_view.xml b/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml index 7cf1789961d3..05ca2a786e3a 100644 --- a/packages/SystemUI/res/layout/auth_biometric_face_to_fingerprint_view.xml +++ b/packages/SystemUI/res/layout/auth_biometric_fingerprint_and_face_view.xml @@ -14,12 +14,13 @@ ~ limitations under the License. --> -<com.android.systemui.biometrics.AuthBiometricFaceToFingerprintView +<com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/contents" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <include layout="@layout/auth_biometric_contents"/> -</com.android.systemui.biometrics.AuthBiometricFaceToFingerprintView>
\ No newline at end of file +</com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_biometric_udfps_view.xml b/packages/SystemUI/res/layout/auth_biometric_view.xml index 238288eb9f69..ee4da25f2284 100644 --- a/packages/SystemUI/res/layout/auth_biometric_udfps_view.xml +++ b/packages/SystemUI/res/layout/auth_biometric_view.xml @@ -1,5 +1,5 @@ <!-- - ~ Copyright (C) 2020 The Android Open Source Project + ~ 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. @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<com.android.systemui.biometrics.AuthBiometricUdfpsView +<com.android.systemui.biometrics.AuthBiometricView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/contents" android:layout_width="match_parent" @@ -23,4 +23,4 @@ <include layout="@layout/auth_biometric_contents"/> -</com.android.systemui.biometrics.AuthBiometricUdfpsView>
\ No newline at end of file +</com.android.systemui.biometrics.AuthBiometricView>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 65c17b9028e1..5a7efca3dece 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -876,7 +876,8 @@ <dimen name="remote_input_history_extra_height">60dp</dimen> <!-- Biometric Dialog values --> - <dimen name="biometric_dialog_biometric_icon_size">64dp</dimen> + <dimen name="biometric_dialog_face_icon_size">64dp</dimen> + <dimen name="biometric_dialog_fingerprint_icon_size">80dp</dimen> <dimen name="biometric_dialog_button_negative_max_width">160dp</dimen> <dimen name="biometric_dialog_button_positive_max_width">136dp</dimen> <dimen name="biometric_dialog_corner_size">4dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 6d336e60e1d3..9e1f57bbfa75 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -322,6 +322,8 @@ <string name="biometric_dialog_face_icon_description_confirmed">Confirmed</string> <!-- 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 your face. Press to continue.</string> <!-- Talkback string when a biometric is authenticated [CHAR LIMIT=NONE] --> <string name="biometric_dialog_authenticated">Authenticated</string> diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java index 885a1777f30b..aafbf7e4d439 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnification.java @@ -172,6 +172,17 @@ public class WindowMagnification extends CoreStartable implements WindowMagnifie } @MainThread + void moveWindowMagnifierToPositionInternal(int displayId, float positionX, float positionY, + IRemoteMagnificationAnimationCallback callback) { + final WindowMagnificationController windowMagnificationController = + mMagnificationControllerSupplier.get(displayId); + if (windowMagnificationController != null) { + windowMagnificationController.moveWindowMagnifierToPosition(positionX, positionY, + callback); + } + } + + @MainThread void disableWindowMagnification(int displayId, @Nullable IRemoteMagnificationAnimationCallback callback) { final WindowMagnificationController windowMagnificationController = @@ -210,9 +221,9 @@ public class WindowMagnification extends CoreStartable implements WindowMagnifie } @Override - public void onDrag(int displayId) { + public void onMove(int displayId) { if (mWindowMagnificationConnectionImpl != null) { - mWindowMagnificationConnectionImpl.onDrag(displayId); + mWindowMagnificationConnectionImpl.onMove(displayId); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java index dc1e0054ff24..3b4114bfb984 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java @@ -156,6 +156,7 @@ class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUp } mAnimationCallback = animationCallback; setupEnableAnimationSpecs(scale, centerX, centerY); + if (mEndSpec.equals(mStartSpec)) { if (mState == STATE_DISABLED) { mController.enableWindowMagnificationInternal(scale, centerX, centerY, @@ -178,6 +179,24 @@ class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUp } } + void moveWindowMagnifierToPosition(float centerX, float centerY, + IRemoteMagnificationAnimationCallback callback) { + if (mState == STATE_ENABLED) { + // We set the animation duration to shortAnimTime which would be reset at the end. + mValueAnimator.setDuration(mContext.getResources() + .getInteger(com.android.internal.R.integer.config_shortAnimTime)); + enableWindowMagnification(Float.NaN, centerX, centerY, + /* magnificationFrameOffsetRatioX */ Float.NaN, + /* magnificationFrameOffsetRatioY */ Float.NaN, callback); + } else if (mState == STATE_ENABLING) { + sendAnimationCallback(false); + mAnimationCallback = callback; + mValueAnimator.setDuration(mContext.getResources() + .getInteger(com.android.internal.R.integer.config_shortAnimTime)); + setupEnableAnimationSpecs(Float.NaN, centerX, centerY); + } + } + private void setupEnableAnimationSpecs(float scale, float centerX, float centerY) { if (mController == null) { return; @@ -193,9 +212,16 @@ class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUp R.integer.magnification_default_scale) : scale, centerX, centerY); } else { mStartSpec.set(currentScale, currentCenterX, currentCenterY); - mEndSpec.set(Float.isNaN(scale) ? currentScale : scale, - Float.isNaN(centerX) ? currentCenterX : centerX, - Float.isNaN(centerY) ? currentCenterY : centerY); + + final float endScale = (mState == STATE_ENABLING ? mEndSpec.mScale : currentScale); + final float endCenterX = + (mState == STATE_ENABLING ? mEndSpec.mCenterX : currentCenterX); + final float endCenterY = + (mState == STATE_ENABLING ? mEndSpec.mCenterY : currentCenterY); + + mEndSpec.set(Float.isNaN(scale) ? endScale : scale, + Float.isNaN(centerX) ? endCenterX : centerX, + Float.isNaN(centerY) ? endCenterY : centerY); } if (DEBUG) { Log.d(TAG, "SetupEnableAnimationSpecs : mStartSpec = " + mStartSpec + ", endSpec = " @@ -269,6 +295,9 @@ class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUp setState(STATE_ENABLED); } sendAnimationCallback(true); + // We reset the duration to config_longAnimTime + mValueAnimator.setDuration(mContext.getResources() + .getInteger(com.android.internal.R.integer.config_longAnimTime)); } @Override @@ -313,10 +342,10 @@ class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUp mMagnificationFrameOffsetRatioX, mMagnificationFrameOffsetRatioY); } - private static ValueAnimator newValueAnimator(Resources resources) { + private static ValueAnimator newValueAnimator(Resources resource) { final ValueAnimator valueAnimator = new ValueAnimator(); valueAnimator.setDuration( - resources.getInteger(com.android.internal.R.integer.config_longAnimTime)); + resource.getInteger(com.android.internal.R.integer.config_longAnimTime)); valueAnimator.setInterpolator(new AccelerateInterpolator(2.5f)); valueAnimator.setFloatValues(0.0f, 1.0f); return valueAnimator; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java index 1d22633455e9..aa684faee5ab 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationConnectionImpl.java @@ -77,6 +77,13 @@ class WindowMagnificationConnectionImpl extends IWindowMagnificationConnection.S } @Override + public void moveWindowMagnifierToPosition(int displayId, float positionX, float positionY, + IRemoteMagnificationAnimationCallback callback) { + mHandler.post(() -> mWindowMagnification.moveWindowMagnifierToPositionInternal( + displayId, positionX, positionY, callback)); + } + + @Override public void showMagnificationButton(int displayId, int magnificationMode) { mHandler.post( () -> mModeSwitchesController.showButton(displayId, magnificationMode)); @@ -143,10 +150,10 @@ class WindowMagnificationConnectionImpl extends IWindowMagnificationConnection.S } } - void onDrag(int displayId) { + void onMove(int displayId) { if (mConnectionCallback != null) { try { - mConnectionCallback.onDrag(displayId); + mConnectionCallback.onMove(displayId); } catch (RemoteException e) { Log.e(TAG, "Failed to inform taking control by a user", e); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index de03993a6f17..50ca447090b5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -852,6 +852,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold @Override public void move(int xOffset, int yOffset) { moveWindowMagnifier(xOffset, yOffset); + mWindowMagnifierCallback.onMove(mDisplayId); } /** @@ -985,6 +986,14 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } } + void moveWindowMagnifierToPosition(float positionX, float positionY, + IRemoteMagnificationAnimationCallback callback) { + if (mMirrorSurfaceView == null) { + return; + } + mAnimationController.moveWindowMagnifierToPosition(positionX, positionY, callback); + } + /** * Gets the scale. * @@ -1037,8 +1046,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold @Override public boolean onDrag(float offsetX, float offsetY) { - moveWindowMagnifier(offsetX, offsetY); - mWindowMagnifierCallback.onDrag(mDisplayId); + move((int) offsetX, (int) offsetY); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java index bdded10dfa1d..c334ca664c46 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnifierCallback.java @@ -17,7 +17,6 @@ package com.android.systemui.accessibility; import android.graphics.Rect; -import android.view.ViewConfiguration; /** * A callback to inform {@link com.android.server.accessibility.AccessibilityManagerService} about @@ -56,11 +55,9 @@ interface WindowMagnifierCallback { void onAccessibilityActionPerformed(int displayId); /** - * Called when the user is performing dragging gesture. It is started after the offset - * between the down location and the move event location exceed - * {@link ViewConfiguration#getScaledTouchSlop()}. + * Called when the user is performing a move action. * * @param displayId The logical display id. */ - void onDrag(int displayId); + void onMove(int displayId); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceIconController.kt new file mode 100644 index 000000000000..55611f7d7ada --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceIconController.kt @@ -0,0 +1,123 @@ +/* + * 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.content.Context +import android.graphics.drawable.Drawable +import android.util.Log +import android.widget.ImageView +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_AUTHENTICATING +import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN +import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR +import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP +import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE +import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION + +private const val TAG = "AuthBiometricFaceIconController" + +/** Face only icon animator for BiometricPrompt. */ +class AuthBiometricFaceIconController( + context: Context, + iconView: ImageView +) : AuthIconController(context, iconView) { + + // false = dark to light, true = light to dark + private var lastPulseLightToDark = false + + @BiometricState + private var state = 0 + + init { + val size = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) + iconView.layoutParams.width = size + iconView.layoutParams.height = size + showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light) + } + + private fun startPulsing() { + lastPulseLightToDark = false + animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true) + } + + private fun pulseInNextDirection() { + val iconRes = if (lastPulseLightToDark) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + animateIcon(iconRes, true /* repeat */) + lastPulseLightToDark = !lastPulseLightToDark + } + + override fun handleAnimationEnd(drawable: Drawable) { + if (state == STATE_AUTHENTICATING || state == STATE_HELP) { + pulseInNextDirection() + } + } + + override fun updateIcon(@BiometricState oldState: Int, @BiometricState newState: Int) { + val lastStateIsErrorIcon = (oldState == STATE_ERROR || oldState == STATE_HELP) + if (newState == STATE_AUTHENTICATING_ANIMATING_IN) { + showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light) + iconView.contentDescription = context.getString( + R.string.biometric_dialog_face_icon_description_authenticating + ) + } else if (newState == STATE_AUTHENTICATING) { + startPulsing() + iconView.contentDescription = context.getString( + R.string.biometric_dialog_face_icon_description_authenticating + ) + } else if (oldState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) { + animateIconOnce(R.drawable.face_dialog_dark_to_checkmark) + iconView.contentDescription = context.getString( + R.string.biometric_dialog_face_icon_description_confirmed + ) + } else if (lastStateIsErrorIcon && newState == STATE_IDLE) { + animateIconOnce(R.drawable.face_dialog_error_to_idle) + iconView.contentDescription = context.getString( + R.string.biometric_dialog_face_icon_description_idle + ) + } else if (lastStateIsErrorIcon && newState == STATE_AUTHENTICATED) { + animateIconOnce(R.drawable.face_dialog_dark_to_checkmark) + iconView.contentDescription = context.getString( + R.string.biometric_dialog_face_icon_description_authenticated + ) + } else if (newState == STATE_ERROR && oldState != STATE_ERROR) { + animateIconOnce(R.drawable.face_dialog_dark_to_error) + } else if (oldState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) { + animateIconOnce(R.drawable.face_dialog_dark_to_checkmark) + iconView.contentDescription = context.getString( + R.string.biometric_dialog_face_icon_description_authenticated + ) + } else if (newState == STATE_PENDING_CONFIRMATION) { + animateIconOnce(R.drawable.face_dialog_wink_from_dark) + iconView.contentDescription = context.getString( + R.string.biometric_dialog_face_icon_description_authenticated + ) + } else if (newState == STATE_IDLE) { + showStaticDrawable(R.drawable.face_dialog_idle_static) + iconView.contentDescription = context.getString( + R.string.biometric_dialog_face_icon_description_idle + ) + } else { + Log.w(TAG, "Unhandled state: $newState") + } + state = newState + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java deleted file mode 100644 index ae3e94b9a1cb..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintView.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (C) 2021 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 static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; -import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; - -import android.content.Context; -import android.hardware.biometrics.BiometricAuthenticator.Modality; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; -import android.os.Bundle; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.R; - -/** - * Manages the layout of an auth dialog for devices with both a face sensor and a fingerprint - * sensor. Face authentication is attempted first, followed by fingerprint if the initial attempt is - * unsuccessful. - */ -public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView { - private static final String TAG = "BiometricPrompt/AuthBiometricFaceToFingerprintView"; - - protected static class UdfpsIconController extends IconController { - @BiometricState private int mIconState = STATE_IDLE; - - protected UdfpsIconController( - @NonNull Context context, @NonNull ImageView iconView, @NonNull TextView textView) { - super(context, iconView, textView); - } - - void updateState(@BiometricState int newState) { - updateState(mIconState, newState); - } - - @Override - protected void updateState(int lastState, int newState) { - final boolean lastStateIsErrorIcon = - lastState == STATE_ERROR || lastState == STATE_HELP; - - switch (newState) { - case STATE_IDLE: - case STATE_AUTHENTICATING_ANIMATING_IN: - case STATE_AUTHENTICATING: - case STATE_PENDING_CONFIRMATION: - case STATE_AUTHENTICATED: - if (lastStateIsErrorIcon) { - animateOnce(R.drawable.fingerprint_dialog_error_to_fp); - } else { - showStaticDrawable(R.drawable.fingerprint_dialog_fp_to_error); - } - mIconView.setContentDescription(mContext.getString( - R.string.accessibility_fingerprint_dialog_fingerprint_icon)); - break; - - case STATE_ERROR: - case STATE_HELP: - if (!lastStateIsErrorIcon) { - animateOnce(R.drawable.fingerprint_dialog_fp_to_error); - } else { - showStaticDrawable(R.drawable.fingerprint_dialog_error_to_fp); - } - mIconView.setContentDescription(mContext.getString( - R.string.biometric_dialog_try_again)); - break; - - default: - Log.e(TAG, "Unknown biometric dialog state: " + newState); - break; - } - - mState = newState; - mIconState = newState; - } - } - - @Modality private int mActiveSensorType = TYPE_FACE; - @Nullable private ModalityListener mModalityListener; - @Nullable private FingerprintSensorPropertiesInternal mFingerprintSensorProps; - @Nullable private UdfpsDialogMeasureAdapter mUdfpsMeasureAdapter; - @Nullable @VisibleForTesting UdfpsIconController mUdfpsIconController; - - - public AuthBiometricFaceToFingerprintView(Context context) { - super(context); - } - - public AuthBiometricFaceToFingerprintView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @VisibleForTesting - AuthBiometricFaceToFingerprintView(Context context, AttributeSet attrs, Injector injector) { - super(context, attrs, injector); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mUdfpsIconController = new UdfpsIconController(mContext, mIconView, mIndicatorView); - } - - @Modality - int getActiveSensorType() { - return mActiveSensorType; - } - - boolean isFingerprintUdfps() { - return mFingerprintSensorProps.isAnyUdfpsType(); - } - - void setModalityListener(@NonNull ModalityListener listener) { - mModalityListener = listener; - } - - void setFingerprintSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) { - mFingerprintSensorProps = sensorProps; - } - - @Override - protected int getDelayAfterAuthenticatedDurationMs() { - return mActiveSensorType == TYPE_FINGERPRINT ? 0 - : super.getDelayAfterAuthenticatedDurationMs(); - } - - @Override - protected boolean supportsManualRetry() { - return false; - } - - @Override - public void onAuthenticationFailed( - @Modality int modality, @Nullable String failureReason) { - super.onAuthenticationFailed(modality, checkErrorForFallback(failureReason)); - } - - @Override - public void onError(int modality, String error) { - super.onError(modality, checkErrorForFallback(error)); - } - - private String checkErrorForFallback(String message) { - if (mActiveSensorType == TYPE_FACE) { - Log.d(TAG, "Falling back to fingerprint: " + message); - - // switching from face -> fingerprint mode, suppress root error messages - mCallback.onAction(Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR); - return mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead); - } - return message; - } - - @Override - @BiometricState - protected int getStateForAfterError() { - if (mActiveSensorType == TYPE_FACE) { - return STATE_AUTHENTICATING; - } - - return super.getStateForAfterError(); - } - - @Override - public void updateState(@BiometricState int newState) { - if (mActiveSensorType == TYPE_FACE) { - if (newState == STATE_HELP || newState == STATE_ERROR) { - mActiveSensorType = TYPE_FINGERPRINT; - - setRequireConfirmation(false); - mConfirmButton.setEnabled(false); - mConfirmButton.setVisibility(View.GONE); - - if (mModalityListener != null) { - mModalityListener.onModalitySwitched(TYPE_FACE, mActiveSensorType); - } - - // Deactivate the face icon controller so it stops drawing to the view - mFaceIconController.deactivate(); - // Then, activate this icon controller. We need to start in the "idle" state - mUdfpsIconController.updateState(STATE_IDLE); - } - } else { // Fingerprint - mUdfpsIconController.updateState(newState); - } - - super.updateState(newState); - } - - @Override - @NonNull - AuthDialog.LayoutParams onMeasureInternal(int width, int height) { - final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height); - return isFingerprintUdfps() - ? getUdfpsMeasureAdapter().onMeasureInternal(width, height, layoutParams) - : layoutParams; - } - - @NonNull - private UdfpsDialogMeasureAdapter getUdfpsMeasureAdapter() { - if (mUdfpsMeasureAdapter == null - || mUdfpsMeasureAdapter.getSensorProps() != mFingerprintSensorProps) { - mUdfpsMeasureAdapter = new UdfpsDialogMeasureAdapter(this, mFingerprintSensorProps); - } - return mUdfpsMeasureAdapter; - } - - @Override - public void onSaveState(@NonNull Bundle outState) { - super.onSaveState(outState); - outState.putInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, mActiveSensorType); - outState.putParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS, mFingerprintSensorProps); - } - - @Override - public void restoreState(@Nullable Bundle savedState) { - super.restoreState(savedState); - if (savedState != null) { - mActiveSensorType = savedState.getInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, TYPE_FACE); - mFingerprintSensorProps = - savedState.getParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java deleted file mode 100644 index 48f6431aec69..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import android.content.Context; -import android.graphics.drawable.Animatable2; -import android.graphics.drawable.AnimatedVectorDrawable; -import android.graphics.drawable.Drawable; -import android.hardware.biometrics.BiometricAuthenticator.Modality; -import android.os.Handler; -import android.os.Looper; -import android.util.AttributeSet; -import android.util.Log; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.R; - -public class AuthBiometricFaceView extends AuthBiometricView { - - private static final String TAG = "BiometricPrompt/AuthBiometricFaceView"; - - // Delay before dismissing after being authenticated/confirmed. - private static final int HIDE_DELAY_MS = 500; - - protected static class IconController extends Animatable2.AnimationCallback { - protected Context mContext; - protected ImageView mIconView; - protected TextView mTextView; - protected Handler mHandler; - protected boolean mLastPulseLightToDark; // false = dark to light, true = light to dark - protected @BiometricState int mState; - protected boolean mDeactivated; - - protected IconController(Context context, ImageView iconView, TextView textView) { - mContext = context; - mIconView = iconView; - mTextView = textView; - mHandler = new Handler(Looper.getMainLooper()); - showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light); - } - - protected void animateOnce(int iconRes) { - animateIcon(iconRes, false); - } - - protected void showStaticDrawable(int iconRes) { - mIconView.setImageDrawable(mContext.getDrawable(iconRes)); - } - - protected void animateIcon(int iconRes, boolean repeat) { - Log.d(TAG, "animateIcon, state: " + mState + ", deactivated: " + mDeactivated); - if (mDeactivated) { - return; - } - - final AnimatedVectorDrawable icon = - (AnimatedVectorDrawable) mContext.getDrawable(iconRes); - mIconView.setImageDrawable(icon); - icon.forceAnimationOnUI(); - if (repeat) { - icon.registerAnimationCallback(this); - } - icon.start(); - } - - protected void startPulsing() { - mLastPulseLightToDark = false; - animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true); - } - - protected void pulseInNextDirection() { - int iconRes = mLastPulseLightToDark ? R.drawable.face_dialog_pulse_dark_to_light - : R.drawable.face_dialog_pulse_light_to_dark; - animateIcon(iconRes, true /* repeat */); - mLastPulseLightToDark = !mLastPulseLightToDark; - } - - @Override - public void onAnimationEnd(Drawable drawable) { - super.onAnimationEnd(drawable); - Log.d(TAG, "onAnimationEnd, mState: " + mState + ", deactivated: " + mDeactivated); - if (mDeactivated) { - return; - } - - if (mState == STATE_AUTHENTICATING || mState == STATE_HELP) { - pulseInNextDirection(); - } - } - - protected void deactivate() { - mDeactivated = true; - } - - protected void updateState(int lastState, int newState) { - if (mDeactivated) { - Log.w(TAG, "Ignoring updateState when deactivated: " + newState); - return; - } - - final boolean lastStateIsErrorIcon = - lastState == STATE_ERROR || lastState == STATE_HELP; - - if (newState == STATE_AUTHENTICATING_ANIMATING_IN) { - showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light); - mIconView.setContentDescription(mContext.getString( - R.string.biometric_dialog_face_icon_description_authenticating)); - } else if (newState == STATE_AUTHENTICATING) { - startPulsing(); - mIconView.setContentDescription(mContext.getString( - R.string.biometric_dialog_face_icon_description_authenticating)); - } else if (lastState == STATE_PENDING_CONFIRMATION && newState == STATE_AUTHENTICATED) { - animateOnce(R.drawable.face_dialog_dark_to_checkmark); - mIconView.setContentDescription(mContext.getString( - R.string.biometric_dialog_face_icon_description_confirmed)); - } else if (lastStateIsErrorIcon && newState == STATE_IDLE) { - animateOnce(R.drawable.face_dialog_error_to_idle); - mIconView.setContentDescription(mContext.getString( - R.string.biometric_dialog_face_icon_description_idle)); - } else if (lastStateIsErrorIcon && newState == STATE_AUTHENTICATED) { - animateOnce(R.drawable.face_dialog_dark_to_checkmark); - mIconView.setContentDescription(mContext.getString( - R.string.biometric_dialog_face_icon_description_authenticated)); - } else if (newState == STATE_ERROR && lastState != STATE_ERROR) { - animateOnce(R.drawable.face_dialog_dark_to_error); - } else if (lastState == STATE_AUTHENTICATING && newState == STATE_AUTHENTICATED) { - animateOnce(R.drawable.face_dialog_dark_to_checkmark); - mIconView.setContentDescription(mContext.getString( - R.string.biometric_dialog_face_icon_description_authenticated)); - } else if (newState == STATE_PENDING_CONFIRMATION) { - animateOnce(R.drawable.face_dialog_wink_from_dark); - mIconView.setContentDescription(mContext.getString( - R.string.biometric_dialog_face_icon_description_authenticated)); - } else if (newState == STATE_IDLE) { - showStaticDrawable(R.drawable.face_dialog_idle_static); - mIconView.setContentDescription(mContext.getString( - R.string.biometric_dialog_face_icon_description_idle)); - } else { - Log.w(TAG, "Unhandled state: " + newState); - } - mState = newState; - } - } - - @Nullable @VisibleForTesting IconController mFaceIconController; - @NonNull private final OnAttachStateChangeListener mOnAttachStateChangeListener = - new OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - - } - - @Override - public void onViewDetachedFromWindow(View v) { - mFaceIconController.deactivate(); - } - }; - - public AuthBiometricFaceView(Context context) { - this(context, null); - } - - public AuthBiometricFaceView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @VisibleForTesting - AuthBiometricFaceView(Context context, AttributeSet attrs, Injector injector) { - super(context, attrs, injector); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mFaceIconController = new IconController(mContext, mIconView, mIndicatorView); - - addOnAttachStateChangeListener(mOnAttachStateChangeListener); - } - - @Override - protected int getDelayAfterAuthenticatedDurationMs() { - return HIDE_DELAY_MS; - } - - @Override - protected int getStateForAfterError() { - return STATE_IDLE; - } - - @Override - protected void handleResetAfterError() { - resetErrorView(); - } - - @Override - protected void handleResetAfterHelp() { - resetErrorView(); - } - - @Override - protected boolean supportsSmallDialog() { - return true; - } - - @Override - protected boolean supportsManualRetry() { - return true; - } - - @Override - public void updateState(@BiometricState int newState) { - mFaceIconController.updateState(mState, newState); - - if (newState == STATE_AUTHENTICATING_ANIMATING_IN || - (newState == STATE_AUTHENTICATING && getSize() == AuthDialog.SIZE_MEDIUM)) { - resetErrorView(); - } - - // Do this last since the state variable gets updated. - super.updateState(newState); - } - - @Override - public void onAuthenticationFailed(@Modality int modality, @Nullable String failureReason) { - if (getSize() == AuthDialog.SIZE_MEDIUM) { - if (supportsManualRetry()) { - mTryAgainButton.setVisibility(View.VISIBLE); - mConfirmButton.setVisibility(View.GONE); - } - } - - // Do this last since we want to know if the button is being animated (in the case of - // small -> medium dialog) - super.onAuthenticationFailed(modality, failureReason); - } - - private void resetErrorView() { - mIndicatorView.setTextColor(mTextColorHint); - mIndicatorView.setVisibility(View.INVISIBLE); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.kt new file mode 100644 index 000000000000..be89d10393dd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.biometrics + +import android.content.Context +import android.hardware.biometrics.BiometricAuthenticator.Modality +import android.util.AttributeSet + +/** Face only view for BiometricPrompt. */ +class AuthBiometricFaceView( + context: Context, + attrs: AttributeSet? = null +) : AuthBiometricView(context, attrs) { + + override fun getDelayAfterAuthenticatedDurationMs() = HIDE_DELAY_MS + + override fun getStateForAfterError() = STATE_IDLE + + override fun handleResetAfterError() = resetErrorView() + + override fun handleResetAfterHelp() = resetErrorView() + + override fun supportsSmallDialog() = true + + override fun supportsManualRetry() = true + + override fun supportsRequireConfirmation() = true + + override fun createIconController(): AuthIconController = + AuthBiometricFaceIconController(mContext, mIconView) + + override fun updateState(@BiometricState newState: Int) { + if (newState == STATE_AUTHENTICATING_ANIMATING_IN || + newState == STATE_AUTHENTICATING && size == AuthDialog.SIZE_MEDIUM) { + resetErrorView() + } + + // Do this last since the state variable gets updated. + super.updateState(newState) + } + + override fun onAuthenticationFailed( + @Modality modality: Int, + failureReason: String? + ) { + if (size == AuthDialog.SIZE_MEDIUM) { + if (supportsManualRetry()) { + mTryAgainButton.visibility = VISIBLE + mConfirmButton.visibility = GONE + } + } + + // Do this last since we want to know if the button is being animated (in the case of + // small -> medium dialog) + super.onAuthenticationFailed(modality, failureReason) + } + + private fun resetErrorView() { + mIndicatorView.setTextColor(mTextColorHint) + mIndicatorView.visibility = INVISIBLE + } + + companion object { + /** Delay before dismissing after being authenticated/confirmed. */ + const val HIDE_DELAY_MS = 500 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt new file mode 100644 index 000000000000..3e4e573c9531 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt @@ -0,0 +1,60 @@ +/* + * 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.content.Context +import android.graphics.drawable.Drawable +import android.widget.ImageView +import com.android.systemui.R +import com.android.systemui.biometrics.AuthBiometricView.BiometricState +import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION +import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATED +import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR +import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP + +/** Face/Fingerprint combined icon animator for BiometricPrompt. */ +class AuthBiometricFingerprintAndFaceIconController( + context: Context, + iconView: ImageView +) : AuthBiometricFingerprintIconController(context, iconView) { + + override val actsAsConfirmButton: Boolean = true + + override fun shouldAnimateForTransition( + @BiometricState oldState: Int, + @BiometricState newState: Int + ): Boolean = when (newState) { + STATE_PENDING_CONFIRMATION -> true + STATE_AUTHENTICATED -> false + else -> super.shouldAnimateForTransition(oldState, newState) + } + + override fun getAnimationForTransition( + @BiometricState oldState: Int, + @BiometricState newState: Int + ): Drawable? = when (newState) { + STATE_PENDING_CONFIRMATION -> { + if (oldState == STATE_ERROR || oldState == STATE_HELP) { + context.getDrawable(R.drawable.fingerprint_dialog_error_to_unlock) + } else { + context.getDrawable(R.drawable.fingerprint_dialog_fp_to_unlock) + } + } + 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 new file mode 100644 index 000000000000..7371442bdd07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceView.kt @@ -0,0 +1,43 @@ +/* + * 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.content.Context +import android.hardware.biometrics.BiometricAuthenticator.Modality +import android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE +import android.util.AttributeSet +import com.android.systemui.R + +/** Face/Fingerprint combined view for BiometricPrompt. */ +class AuthBiometricFingerprintAndFaceView( + context: Context, + attrs: AttributeSet? +) : AuthBiometricFingerprintView(context, attrs) { + + constructor (context: Context) : this(context, null) + + override fun getConfirmationPrompt() = R.string.biometric_dialog_tap_confirm_with_face + + override fun forceRequireConfirmation(@Modality modality: Int) = modality == TYPE_FACE + + override fun ignoreUnsuccessfulEventsFrom(@Modality modality: Int) = modality == TYPE_FACE + + override fun onPointerDown(failedModalities: Set<Int>) = failedModalities.contains(TYPE_FACE) + + override fun createIconController(): AuthIconController = + AuthBiometricFingerprintAndFaceIconController(mContext, mIconView) +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt new file mode 100644 index 000000000000..cd16379cd5b4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt @@ -0,0 +1,112 @@ +/* + * 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.content.Context +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import android.widget.ImageView +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_AUTHENTICATING +import com.android.systemui.biometrics.AuthBiometricView.STATE_AUTHENTICATING_ANIMATING_IN +import com.android.systemui.biometrics.AuthBiometricView.STATE_ERROR +import com.android.systemui.biometrics.AuthBiometricView.STATE_HELP +import com.android.systemui.biometrics.AuthBiometricView.STATE_IDLE +import com.android.systemui.biometrics.AuthBiometricView.STATE_PENDING_CONFIRMATION + +/** Fingerprint only icon animator for BiometricPrompt. */ +open class AuthBiometricFingerprintIconController( + context: Context, + iconView: ImageView +) : AuthIconController(context, iconView) { + + init { + val size = context.resources.getDimensionPixelSize( + R.dimen.biometric_dialog_fingerprint_icon_size + ) + iconView.layoutParams.width = size + iconView.layoutParams.height = size + } + + override fun updateIcon(@BiometricState lastState: Int, @BiometricState newState: Int) { + val icon = getAnimationForTransition(lastState, newState) ?: return + + iconView.setImageDrawable(icon) + + val iconContentDescription = getIconContentDescription(newState) + if (iconContentDescription != null) { + iconView.contentDescription = iconContentDescription + } + + (icon as? AnimatedVectorDrawable)?.apply { + reset() + if (shouldAnimateForTransition(lastState, newState)) { + forceAnimationOnUI() + start() + } + } + } + + private fun getIconContentDescription(@BiometricState newState: Int): CharSequence? { + val id = when (newState) { + STATE_IDLE, + STATE_AUTHENTICATING_ANIMATING_IN, + STATE_AUTHENTICATING, + STATE_PENDING_CONFIRMATION, + STATE_AUTHENTICATED -> R.string.accessibility_fingerprint_dialog_fingerprint_icon + STATE_ERROR, + STATE_HELP -> R.string.biometric_dialog_try_again + else -> null + } + return if (id != null) context.getString(id) else null + } + + protected open fun shouldAnimateForTransition( + @BiometricState oldState: Int, + @BiometricState newState: Int + ) = when (newState) { + STATE_HELP, + STATE_ERROR -> true + STATE_AUTHENTICATING_ANIMATING_IN, + STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP + STATE_AUTHENTICATED -> false + else -> false + } + + protected open fun getAnimationForTransition( + @BiometricState oldState: Int, + @BiometricState newState: Int + ): Drawable? { + val id = when (newState) { + STATE_HELP, + STATE_ERROR -> R.drawable.fingerprint_dialog_fp_to_error + STATE_AUTHENTICATING_ANIMATING_IN, + STATE_AUTHENTICATING -> { + if (oldState == STATE_ERROR || oldState == STATE_HELP) { + R.drawable.fingerprint_dialog_error_to_fp + } else { + R.drawable.fingerprint_dialog_fp_to_error + } + } + STATE_AUTHENTICATED -> R.drawable.fingerprint_dialog_fp_to_error + else -> return null + } + return if (id != null) context.getDrawable(id) else null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.java deleted file mode 100644 index ee602bc9cb78..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - - -import android.content.Context; -import android.graphics.drawable.AnimatedVectorDrawable; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.Log; - -import androidx.annotation.Nullable; - -import com.android.systemui.R; - -public class AuthBiometricFingerprintView extends AuthBiometricView { - - private static final String TAG = "BiometricPrompt/AuthBiometricFingerprintView"; - - public AuthBiometricFingerprintView(Context context) { - this(context, null); - } - - public AuthBiometricFingerprintView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected int getDelayAfterAuthenticatedDurationMs() { - return 0; - } - - @Override - protected int getStateForAfterError() { - return STATE_AUTHENTICATING; - } - - @Override - protected void handleResetAfterError() { - showTouchSensorString(); - } - - @Override - protected void handleResetAfterHelp() { - showTouchSensorString(); - } - - @Override - protected boolean supportsSmallDialog() { - return false; - } - - @Override - public void updateState(@BiometricState int newState) { - updateIcon(mState, newState); - - // Do this last since the state variable gets updated. - super.updateState(newState); - } - - @Override - void onAttachedToWindowInternal() { - super.onAttachedToWindowInternal(); - showTouchSensorString(); - } - - private void showTouchSensorString() { - mIndicatorView.setText(R.string.fingerprint_dialog_touch_sensor); - mIndicatorView.setTextColor(mTextColorHint); - } - - private void updateIcon(int lastState, int newState) { - final Drawable icon = getAnimationForTransition(lastState, newState); - if (icon == null) { - Log.e(TAG, "Animation not found, " + lastState + " -> " + newState); - return; - } - - final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable - ? (AnimatedVectorDrawable) icon - : null; - - mIconView.setImageDrawable(icon); - - final CharSequence iconContentDescription = getIconContentDescription(newState); - if (iconContentDescription != null) { - mIconView.setContentDescription(iconContentDescription); - } - - if (animation != null && shouldAnimateForTransition(lastState, newState)) { - animation.forceAnimationOnUI(); - animation.start(); - } - } - - @Nullable - private CharSequence getIconContentDescription(int newState) { - switch (newState) { - case STATE_IDLE: - case STATE_AUTHENTICATING_ANIMATING_IN: - case STATE_AUTHENTICATING: - case STATE_PENDING_CONFIRMATION: - case STATE_AUTHENTICATED: - return mContext.getString( - R.string.accessibility_fingerprint_dialog_fingerprint_icon); - - case STATE_ERROR: - case STATE_HELP: - return mContext.getString(R.string.biometric_dialog_try_again); - - default: - return null; - } - } - - private boolean shouldAnimateForTransition(int oldState, int newState) { - switch (newState) { - case STATE_HELP: - case STATE_ERROR: - return true; - case STATE_AUTHENTICATING_ANIMATING_IN: - case STATE_AUTHENTICATING: - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - return true; - } else { - return false; - } - case STATE_AUTHENTICATED: - return false; - default: - return false; - } - } - - private Drawable getAnimationForTransition(int oldState, int newState) { - int iconRes; - - switch (newState) { - case STATE_HELP: - case STATE_ERROR: - iconRes = R.drawable.fingerprint_dialog_fp_to_error; - break; - case STATE_AUTHENTICATING_ANIMATING_IN: - case STATE_AUTHENTICATING: - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - iconRes = R.drawable.fingerprint_dialog_error_to_fp; - } else { - iconRes = R.drawable.fingerprint_dialog_fp_to_error; - } - break; - case STATE_AUTHENTICATED: - iconRes = R.drawable.fingerprint_dialog_fp_to_error; - break; - default: - return null; - } - - return mContext.getDrawable(iconRes); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.kt new file mode 100644 index 000000000000..368bc3aadb70 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintView.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.biometrics + +import android.content.Context +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal +import android.util.AttributeSet +import android.util.Log +import android.widget.FrameLayout +import android.widget.TextView +import com.android.systemui.R + +private const val TAG = "AuthBiometricFingerprintView" + +/** Fingerprint only view for BiometricPrompt. */ +open class AuthBiometricFingerprintView( + context: Context, + attrs: AttributeSet? = null +) : AuthBiometricView(context, attrs) { + /** If this view is for a UDFPS sensor. */ + var isUdfps = false + private set + + private var udfpsAdapter: UdfpsDialogMeasureAdapter? = null + + /** Set the [sensorProps] of this sensor so the view can be customized prior to layout. */ + fun setSensorProperties(sensorProps: FingerprintSensorPropertiesInternal) { + isUdfps = sensorProps.isAnyUdfpsType + udfpsAdapter = if (isUdfps) UdfpsDialogMeasureAdapter(this, sensorProps) else null + } + + override fun onMeasureInternal(width: Int, height: Int): AuthDialog.LayoutParams { + val layoutParams = super.onMeasureInternal(width, height) + return udfpsAdapter?.onMeasureInternal(width, height, layoutParams) ?: layoutParams + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + + val adapter = udfpsAdapter + if (adapter != 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. + val bottomSpacerHeight = adapter.bottomSpacerHeight + Log.w(TAG, "bottomSpacerHeight: $bottomSpacerHeight") + if (bottomSpacerHeight < 0) { + val iconFrame = findViewById<FrameLayout>(R.id.biometric_icon_frame)!! + iconFrame.translationY = -bottomSpacerHeight.toFloat() + val indicator = findViewById<TextView>(R.id.indicator)!! + indicator.translationY = -bottomSpacerHeight.toFloat() + } + } + } + + override fun getDelayAfterAuthenticatedDurationMs() = 0 + + override fun getStateForAfterError() = STATE_AUTHENTICATING + + override fun handleResetAfterError() = showTouchSensorString() + + override fun handleResetAfterHelp() = showTouchSensorString() + + override fun supportsSmallDialog() = false + + override fun createIconController(): AuthIconController = + AuthBiometricFingerprintIconController(mContext, mIconView) + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + showTouchSensorString() + } + + private fun showTouchSensorString() { + mIndicatorView.setText(R.string.fingerprint_dialog_touch_sensor) + mIndicatorView.setTextColor(mTextColorHint) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricIconController.kt new file mode 100644 index 000000000000..ce5e600e6a77 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricIconController.kt @@ -0,0 +1,94 @@ +/* + * 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.annotation.DrawableRes +import android.content.Context +import android.graphics.drawable.Animatable2 +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import android.util.Log +import android.widget.ImageView +import com.android.systemui.biometrics.AuthBiometricView.BiometricState + +private const val TAG = "AuthIconController" + +/** Controller for animating the BiometricPrompt icon/affordance. */ +abstract class AuthIconController( + protected val context: Context, + protected val iconView: ImageView +) : Animatable2.AnimationCallback() { + + /** If this controller should ignore events and pause. */ + var deactivated: Boolean = false + + /** If the icon view should be treated as an alternate "confirm" button. */ + open val actsAsConfirmButton: Boolean = false + + final override fun onAnimationStart(drawable: Drawable) { + super.onAnimationStart(drawable) + } + + final override fun onAnimationEnd(drawable: Drawable) { + super.onAnimationEnd(drawable) + + if (!deactivated) { + handleAnimationEnd(drawable) + } + } + + /** Set the icon to a static image. */ + protected fun showStaticDrawable(@DrawableRes iconRes: Int) { + iconView.setImageDrawable(context.getDrawable(iconRes)) + } + + /** Animate a resource. */ + protected fun animateIconOnce(@DrawableRes iconRes: Int) { + animateIcon(iconRes, false) + } + + /** Animate a resource. */ + protected fun animateIcon(@DrawableRes iconRes: Int, repeat: Boolean) { + if (!deactivated) { + val icon = context.getDrawable(iconRes) as AnimatedVectorDrawable + iconView.setImageDrawable(icon) + icon.forceAnimationOnUI() + if (repeat) { + icon.registerAnimationCallback(this) + } + icon.start() + } + } + + /** Update the icon to reflect the [newState]. */ + fun updateState(@BiometricState lastState: Int, @BiometricState newState: Int) { + if (deactivated) { + Log.w(TAG, "Ignoring updateState when deactivated: $newState") + } else { + updateIcon(lastState, newState) + } + } + + /** 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) + + /** Called during [onAnimationEnd] if the controller is not [deactivated]. */ + open fun handleAnimationEnd(drawable: Drawable) {} +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java deleted file mode 100644 index d80d9cc9d62d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2020 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.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; -import android.util.AttributeSet; -import android.util.Log; -import android.widget.FrameLayout; -import android.widget.TextView; - -import com.android.systemui.R; - -/** - * Manages the layout for under-display fingerprint sensors (UDFPS). Ensures that UI elements - * do not overlap with - */ -public class AuthBiometricUdfpsView extends AuthBiometricFingerprintView { - private static final String TAG = "AuthBiometricUdfpsView"; - - @Nullable private UdfpsDialogMeasureAdapter mMeasureAdapter; - - public AuthBiometricUdfpsView(Context context) { - this(context, null /* attrs */); - } - - public AuthBiometricUdfpsView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - void setSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) { - if (mMeasureAdapter == null || mMeasureAdapter.getSensorProps() != sensorProps) { - mMeasureAdapter = new UdfpsDialogMeasureAdapter(this, sensorProps); - } - } - - @Override - @NonNull - AuthDialog.LayoutParams onMeasureInternal(int width, int height) { - final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height); - return mMeasureAdapter != null - ? mMeasureAdapter.onMeasureInternal(width, height, layoutParams) - : layoutParams; - } - - @Override - void onLayoutInternal() { - super.onLayoutInternal(); - - // 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 int bottomSpacerHeight = mMeasureAdapter.getBottomSpacerHeight(); - Log.w(TAG, "bottomSpacerHeight: " + bottomSpacerHeight); - if (bottomSpacerHeight < 0) { - FrameLayout iconFrame = findViewById(R.id.biometric_icon_frame); - iconFrame.setTranslationY(-bottomSpacerHeight); - - TextView indicator = findViewById(R.id.indicator); - indicator.setTranslationY(-bottomSpacerHeight); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index 1496f170dffe..76d4aa839ef3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -25,6 +25,7 @@ import android.animation.ValueAnimator; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.StringRes; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricPrompt; @@ -44,19 +45,21 @@ import android.widget.LinearLayout; import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.widget.LockPatternUtils; import com.android.systemui.R; import java.lang.annotation.Retention; 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. + * Contains the Biometric views (title, subtitle, icon, buttons, etc.) and its controllers. */ -public abstract class AuthBiometricView extends LinearLayout { +public class AuthBiometricView extends LinearLayout { - private static final String TAG = "BiometricPrompt/AuthBiometricView"; + private static final String TAG = "AuthBiometricView"; /** * Authentication hardware idle. @@ -102,13 +105,6 @@ public abstract class AuthBiometricView extends LinearLayout { int ACTION_BUTTON_TRY_AGAIN = 4; int ACTION_ERROR = 5; int ACTION_USE_DEVICE_CREDENTIAL = 6; - /** - * Notify the receiver to start the fingerprint sensor. - * - * This is only applicable to multi-sensor devices that need to delay fingerprint auth - * (i.e face -> fingerprint). - */ - int ACTION_START_DELAYED_FINGERPRINT_SENSOR = 7; /** * When an action has occurred. The caller will only invoke this when the callback should @@ -118,66 +114,9 @@ public abstract class AuthBiometricView extends LinearLayout { void onAction(int action); } - @VisibleForTesting - static class Injector { - AuthBiometricView mBiometricView; - - public Button getNegativeButton() { - return mBiometricView.findViewById(R.id.button_negative); - } - - public Button getCancelButton() { - return mBiometricView.findViewById(R.id.button_cancel); - } - - public Button getUseCredentialButton() { - return mBiometricView.findViewById(R.id.button_use_credential); - } - - public Button getConfirmButton() { - return mBiometricView.findViewById(R.id.button_confirm); - } - - public Button getTryAgainButton() { - return mBiometricView.findViewById(R.id.button_try_again); - } - - public TextView getTitleView() { - return mBiometricView.findViewById(R.id.title); - } - - public TextView getSubtitleView() { - return mBiometricView.findViewById(R.id.subtitle); - } - - public TextView getDescriptionView() { - return mBiometricView.findViewById(R.id.description); - } - - public TextView getIndicatorView() { - return mBiometricView.findViewById(R.id.indicator); - } - - public ImageView getIconView() { - return mBiometricView.findViewById(R.id.biometric_icon); - } - - public View getIconHolderView() { - return mBiometricView.findViewById(R.id.biometric_icon_frame); - } - - public int getDelayAfterError() { - return BiometricPrompt.HIDE_DIALOG_DELAY; - } - - public int getMediumToLargeAnimationDurationMs() { - return AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS; - } - } - - private final Injector mInjector; protected final Handler mHandler; private final AccessibilityManager mAccessibilityManager; + private final LockPatternUtils mLockPatternUtils; protected final int mTextColorError; protected final int mTextColorHint; @@ -195,6 +134,11 @@ public abstract class AuthBiometricView extends LinearLayout { protected ImageView mIconView; protected TextView mIndicatorView; + @VisibleForTesting @NonNull AuthIconController mIconController; + @VisibleForTesting int mAnimationDurationShort = AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS; + @VisibleForTesting int mAnimationDurationLong = AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS; + @VisibleForTesting int mAnimationDurationHideDialog = BiometricPrompt.HIDE_DIALOG_DELAY; + // Negative button position, exclusively for the app-specified behavior @VisibleForTesting Button mNegativeButton; // Negative button position, exclusively for cancelling auth after passive auth success @@ -217,30 +161,7 @@ public abstract class AuthBiometricView extends LinearLayout { protected boolean mDialogSizeAnimating; protected Bundle mSavedState; - /** - * Delay after authentication is confirmed, before the dialog should be animated away. - */ - protected abstract int getDelayAfterAuthenticatedDurationMs(); - /** - * State that the dialog/icon should be in after showing a help message. - */ - protected abstract int getStateForAfterError(); - /** - * Invoked when the error message is being cleared. - */ - protected abstract void handleResetAfterError(); - /** - * Invoked when the help message is being cleared. - */ - protected abstract void handleResetAfterHelp(); - - /** - * @return true if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL} - */ - protected abstract boolean supportsSmallDialog(); - private final Runnable mResetErrorRunnable; - private final Runnable mResetHelpRunnable; private final OnClickListener mBackgroundClickListener = (view) -> { @@ -262,11 +183,6 @@ public abstract class AuthBiometricView extends LinearLayout { } public AuthBiometricView(Context context, AttributeSet attrs) { - this(context, attrs, new Injector()); - } - - @VisibleForTesting - AuthBiometricView(Context context, AttributeSet attrs, Injector injector) { super(context, attrs); mHandler = new Handler(Looper.getMainLooper()); mTextColorError = getResources().getColor( @@ -274,10 +190,8 @@ public abstract class AuthBiometricView extends LinearLayout { mTextColorHint = getResources().getColor( R.color.biometric_dialog_gray, context.getTheme()); - mInjector = injector; - mInjector.mBiometricView = this; - mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + mLockPatternUtils = new LockPatternUtils(context); mResetErrorRunnable = () -> { updateState(getStateForAfterError()); @@ -292,36 +206,91 @@ public abstract class AuthBiometricView extends LinearLayout { }; } - public void setPanelController(AuthPanelController panelController) { + /** Delay after authentication is confirmed, before the dialog should be animated away. */ + protected int getDelayAfterAuthenticatedDurationMs() { + return 0; + } + + /** State that the dialog/icon should be in after showing a help message. */ + protected int getStateForAfterError() { + return STATE_IDLE; + } + + /** Invoked when the error message is being cleared. */ + protected void handleResetAfterError() {} + + /** Invoked when the help message is being cleared. */ + protected void handleResetAfterHelp() {} + + /** True if the dialog supports {@link AuthDialog.DialogSize#SIZE_SMALL}. */ + protected boolean supportsSmallDialog() { + return false; + } + + /** The string to show when the user must tap to confirm via the button or icon. */ + @StringRes + protected int getConfirmationPrompt() { + return R.string.biometric_dialog_tap_confirm; + } + + /** True if require confirmation will be honored when set via the API. */ + protected boolean supportsRequireConfirmation() { + return false; + } + + /** True if confirmation will be required even if it was not supported/requested. */ + protected boolean forceRequireConfirmation(@Modality int modality) { + return false; + } + + /** Ignore all events from this (secondary) modality except successful authentication. */ + protected boolean ignoreUnsuccessfulEventsFrom(@Modality int modality) { + return false; + } + + /** + * Create the controller for managing the icons transitions during the prompt. + * + * Subclass should override. + */ + @NonNull + protected AuthIconController createIconController() { + return new AuthIconController(mContext, mIconView) { + @Override + public void updateIcon(int lastState, int newState) {} + }; + } + + void setPanelController(AuthPanelController panelController) { mPanelController = panelController; } - public void setPromptInfo(PromptInfo promptInfo) { + void setPromptInfo(PromptInfo promptInfo) { mPromptInfo = promptInfo; } - public void setCallback(Callback callback) { + void setCallback(Callback callback) { mCallback = callback; } - public void setBackgroundView(View backgroundView) { + void setBackgroundView(View backgroundView) { backgroundView.setOnClickListener(mBackgroundClickListener); } - public void setUserId(int userId) { + void setUserId(int userId) { mUserId = userId; } - public void setEffectiveUserId(int effectiveUserId) { + void setEffectiveUserId(int effectiveUserId) { mEffectiveUserId = effectiveUserId; } - public void setRequireConfirmation(boolean requireConfirmation) { - mRequireConfirmation = requireConfirmation; + void setRequireConfirmation(boolean requireConfirmation) { + mRequireConfirmation = requireConfirmation && supportsRequireConfirmation(); } @VisibleForTesting - void updateSize(@AuthDialog.DialogSize int newSize) { + final void updateSize(@AuthDialog.DialogSize int newSize) { Log.v(TAG, "Current size: " + mSize + " New size: " + newSize); if (newSize == AuthDialog.SIZE_SMALL) { mTitleView.setVisibility(View.GONE); @@ -376,7 +345,7 @@ public abstract class AuthBiometricView extends LinearLayout { // Choreograph together final AnimatorSet as = new AnimatorSet(); - as.setDuration(AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS); + as.setDuration(mAnimationDurationShort); as.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { @@ -429,7 +398,7 @@ public abstract class AuthBiometricView extends LinearLayout { // Translate at full duration final ValueAnimator translationAnimator = ValueAnimator.ofFloat( biometricView.getY(), biometricView.getY() - translationY); - translationAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs()); + translationAnimator.setDuration(mAnimationDurationLong); translationAnimator.addUpdateListener((animation) -> { final float translation = (float) animation.getAnimatedValue(); biometricView.setTranslationY(translation); @@ -438,7 +407,7 @@ public abstract class AuthBiometricView extends LinearLayout { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - if (biometricView.getParent() != null) { + if (biometricView.getParent() instanceof ViewGroup) { ((ViewGroup) biometricView.getParent()).removeView(biometricView); } mSize = newSize; @@ -447,7 +416,7 @@ public abstract class AuthBiometricView extends LinearLayout { // Opacity to 0 in half duration final ValueAnimator opacityAnimator = ValueAnimator.ofFloat(1, 0); - opacityAnimator.setDuration(mInjector.getMediumToLargeAnimationDurationMs() / 2); + opacityAnimator.setDuration(mAnimationDurationLong / 2); opacityAnimator.addUpdateListener((animation) -> { final float opacity = (float) animation.getAnimatedValue(); biometricView.setAlpha(opacity); @@ -457,7 +426,7 @@ public abstract class AuthBiometricView extends LinearLayout { mPanelController.updateForContentDimensions( mPanelController.getContainerWidth(), mPanelController.getContainerHeight(), - mInjector.getMediumToLargeAnimationDurationMs()); + mAnimationDurationLong); // Start the animations together AnimatorSet as = new AnimatorSet(); @@ -466,7 +435,7 @@ public abstract class AuthBiometricView extends LinearLayout { animators.add(opacityAnimator); as.playTogether(animators); - as.setDuration(mInjector.getMediumToLargeAnimationDurationMs() * 2 / 3); + as.setDuration(mAnimationDurationLong * 2 / 3); as.start(); } else { Log.e(TAG, "Unknown transition from: " + mSize + " to: " + newSize); @@ -481,6 +450,8 @@ public abstract class AuthBiometricView extends LinearLayout { public void updateState(@BiometricState int newState) { Log.v(TAG, "newState: " + newState); + mIconController.updateState(mState, newState); + switch (newState) { case STATE_AUTHENTICATING_ANIMATING_IN: case STATE_AUTHENTICATING: @@ -510,10 +481,11 @@ public abstract class AuthBiometricView extends LinearLayout { mNegativeButton.setVisibility(View.GONE); mCancelButton.setVisibility(View.VISIBLE); mUseCredentialButton.setVisibility(View.GONE); - mConfirmButton.setEnabled(true); - mConfirmButton.setVisibility(View.VISIBLE); + // forced confirmations (multi-sensor) use the icon view as the confirm button + mConfirmButton.setEnabled(mRequireConfirmation); + mConfirmButton.setVisibility(mRequireConfirmation ? View.VISIBLE : View.GONE); mIndicatorView.setTextColor(mTextColorHint); - mIndicatorView.setText(R.string.biometric_dialog_tap_confirm); + mIndicatorView.setText(getConfirmationPrompt()); mIndicatorView.setVisibility(View.VISIBLE); break; @@ -536,9 +508,9 @@ public abstract class AuthBiometricView extends LinearLayout { updateState(STATE_AUTHENTICATING); } - public void onAuthenticationSucceeded() { + public void onAuthenticationSucceeded(@Modality int modality) { removePendingAnimations(); - if (mRequireConfirmation) { + if (mRequireConfirmation || forceRequireConfirmation(modality)) { updateState(STATE_PENDING_CONFIRMATION); } else { updateState(STATE_AUTHENTICATED); @@ -553,6 +525,10 @@ public abstract class AuthBiometricView extends LinearLayout { */ public void onAuthenticationFailed( @Modality int modality, @Nullable String failureReason) { + if (ignoreUnsuccessfulEventsFrom(modality)) { + return; + } + showTemporaryMessage(failureReason, mResetErrorRunnable); updateState(STATE_ERROR); } @@ -564,12 +540,27 @@ public abstract class AuthBiometricView extends LinearLayout { * @param error message */ public void onError(@Modality int modality, String error) { + if (ignoreUnsuccessfulEventsFrom(modality)) { + return; + } + showTemporaryMessage(error, mResetErrorRunnable); updateState(STATE_ERROR); - mHandler.postDelayed(() -> { - mCallback.onAction(Callback.ACTION_ERROR); - }, mInjector.getDelayAfterError()); + mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_ERROR), + mAnimationDurationHideDialog); + } + + /** + * 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; } /** @@ -579,6 +570,9 @@ public abstract class AuthBiometricView extends LinearLayout { * @param help message */ public void onHelp(@Modality int modality, String help) { + if (ignoreUnsuccessfulEventsFrom(modality)) { + return; + } if (mSize != AuthDialog.SIZE_MEDIUM) { Log.w(TAG, "Help received in size: " + mSize); return; @@ -639,7 +633,7 @@ public abstract class AuthBiometricView extends LinearLayout { // select to enable marquee unless a screen reader is enabled mIndicatorView.setSelected(!mAccessibilityManager.isEnabled() || !mAccessibilityManager.isTouchExplorationEnabled()); - mHandler.postDelayed(resetMessageRunnable, mInjector.getDelayAfterError()); + mHandler.postDelayed(resetMessageRunnable, mAnimationDurationHideDialog); Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); } @@ -647,29 +641,22 @@ public abstract class AuthBiometricView extends LinearLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); - onFinishInflateInternal(); - } - /** - * After inflation, but before things like restoreState, onAttachedToWindow, etc. - */ - @VisibleForTesting - void onFinishInflateInternal() { - mTitleView = mInjector.getTitleView(); - mSubtitleView = mInjector.getSubtitleView(); - mDescriptionView = mInjector.getDescriptionView(); - mIconView = mInjector.getIconView(); - mIconHolderView = mInjector.getIconHolderView(); - mIndicatorView = mInjector.getIndicatorView(); + mTitleView = findViewById(R.id.title); + mSubtitleView = findViewById(R.id.subtitle); + mDescriptionView = findViewById(R.id.description); + mIconView = findViewById(R.id.biometric_icon); + mIconHolderView = findViewById(R.id.biometric_icon_frame); + mIndicatorView = findViewById(R.id.indicator); // Negative-side (left) buttons - mNegativeButton = mInjector.getNegativeButton(); - mCancelButton = mInjector.getCancelButton(); - mUseCredentialButton = mInjector.getUseCredentialButton(); + mNegativeButton = findViewById(R.id.button_negative); + mCancelButton = findViewById(R.id.button_cancel); + mUseCredentialButton = findViewById(R.id.button_use_credential); // Positive-side (right) buttons - mConfirmButton = mInjector.getConfirmButton(); - mTryAgainButton = mInjector.getTryAgainButton(); + mConfirmButton = findViewById(R.id.button_confirm); + mTryAgainButton = findViewById(R.id.button_try_again); mNegativeButton.setOnClickListener((view) -> { mCallback.onAction(Callback.ACTION_BUTTON_NEGATIVE); @@ -693,6 +680,15 @@ public abstract class AuthBiometricView extends LinearLayout { mTryAgainButton.setVisibility(View.GONE); Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); }); + + mIconController = createIconController(); + if (mIconController.getActsAsConfirmButton()) { + mIconView.setOnClickListener((view) -> { + if (mState == STATE_PENDING_CONFIRMATION) { + updateState(STATE_AUTHENTICATED); + } + }); + } } /** @@ -706,21 +702,13 @@ public abstract class AuthBiometricView extends LinearLayout { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - onAttachedToWindowInternal(); - } - /** - * Contains all the testable logic that should be invoked when {@link #onAttachedToWindow()} is - * invoked. - */ - @VisibleForTesting - void onAttachedToWindowInternal() { mTitleView.setText(mPromptInfo.getTitle()); if (isDeviceCredentialAllowed()) { final CharSequence credentialButtonText; - final @Utils.CredentialType int credentialType = - Utils.getCredentialType(mContext, mEffectiveUserId); + @Utils.CredentialType final int credentialType = + Utils.getCredentialType(mLockPatternUtils, mEffectiveUserId); switch (credentialType) { case Utils.CREDENTIAL_PIN: credentialButtonText = @@ -731,9 +719,6 @@ public abstract class AuthBiometricView extends LinearLayout { getResources().getString(R.string.biometric_dialog_use_pattern); break; case Utils.CREDENTIAL_PASSWORD: - credentialButtonText = - getResources().getString(R.string.biometric_dialog_use_password); - break; default: credentialButtonText = getResources().getString(R.string.biometric_dialog_use_password); @@ -749,7 +734,6 @@ public abstract class AuthBiometricView extends LinearLayout { } setTextOrHide(mSubtitleView, mPromptInfo.getSubtitle()); - setTextOrHide(mDescriptionView, mPromptInfo.getDescription()); if (mSavedState == null) { @@ -774,6 +758,8 @@ public abstract class AuthBiometricView extends LinearLayout { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); + mIconController.setDeactivated(true); + // Empty the handler, otherwise things like ACTION_AUTHENTICATED may be duplicated once // the new dialog is restored. mHandler.removeCallbacksAndMessages(null /* all */); @@ -856,15 +842,7 @@ public abstract class AuthBiometricView extends LinearLayout { @Override public void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - onLayoutInternal(); - } - /** - * Contains all the testable logic that should be invoked when - * {@link #onLayout(boolean, int, int, int, int)}, is invoked. - */ - @VisibleForTesting - void onLayoutInternal() { // Start with initial size only once. Subsequent layout changes don't matter since we // only care about the initial icon position. if (mIconOriginalY == 0) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 21edb2478c79..6b6af4c7b52f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -16,9 +16,10 @@ package com.android.systemui.biometrics; -import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT; import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; +import android.annotation.DurationMillisLong; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -52,13 +53,16 @@ import android.widget.LinearLayout; import android.widget.ScrollView; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.widget.LockPatternUtils; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.keyguard.WakefulnessLifecycle; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Top level container/controller for the BiometricPrompt UI. @@ -66,54 +70,52 @@ import java.util.List; public class AuthContainerView extends LinearLayout implements AuthDialog, WakefulnessLifecycle.Observer { - private static final String TAG = "BiometricPrompt/AuthContainerView"; + private static final String TAG = "AuthContainerView"; + private static final int ANIMATION_DURATION_SHOW_MS = 250; - private static final int ANIMATION_DURATION_AWAY_MS = 350; // ms + private static final int ANIMATION_DURATION_AWAY_MS = 350; - static final int STATE_UNKNOWN = 0; - static final int STATE_ANIMATING_IN = 1; - static final int STATE_PENDING_DISMISS = 2; - static final int STATE_SHOWING = 3; - static final int STATE_ANIMATING_OUT = 4; - static final int STATE_GONE = 5; + private static final int STATE_UNKNOWN = 0; + private static final int STATE_ANIMATING_IN = 1; + private static final int STATE_PENDING_DISMISS = 2; + private static final int STATE_SHOWING = 3; + private static final int STATE_ANIMATING_OUT = 4; + private static final int STATE_GONE = 5; @Retention(RetentionPolicy.SOURCE) @IntDef({STATE_UNKNOWN, STATE_ANIMATING_IN, STATE_PENDING_DISMISS, STATE_SHOWING, STATE_ANIMATING_OUT, STATE_GONE}) - @interface ContainerState {} + private @interface ContainerState {} - final Config mConfig; - final int mEffectiveUserId; - @Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps; - @Nullable private final List<FaceSensorPropertiesInternal> mFaceProps; + private final Config mConfig; + private final int mEffectiveUserId; private final Handler mHandler; - private final Injector mInjector; private final IBinder mWindowToken = new Binder(); private final WindowManager mWindowManager; - private final AuthPanelController mPanelController; private final Interpolator mLinearOutSlowIn; - @VisibleForTesting final BiometricCallback mBiometricCallback; private final CredentialCallback mCredentialCallback; + private final LockPatternUtils mLockPatternUtils; + private final WakefulnessLifecycle mWakefulnessLifecycle; - @VisibleForTesting final FrameLayout mFrameLayout; - @VisibleForTesting @Nullable AuthBiometricView mBiometricView; - @VisibleForTesting @Nullable AuthCredentialView mCredentialView; + @VisibleForTesting final BiometricCallback mBiometricCallback; - @VisibleForTesting final ImageView mBackgroundView; - @VisibleForTesting final ScrollView mBiometricScrollView; + @Nullable private AuthBiometricView mBiometricView; + @Nullable private AuthCredentialView mCredentialView; + private final AuthPanelController mPanelController; + private final FrameLayout mFrameLayout; + private final ImageView mBackgroundView; + private final ScrollView mBiometricScrollView; private final View mPanelView; - private final float mTranslationY; - - private final WakefulnessLifecycle mWakefulnessLifecycle; - - @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN; + @ContainerState private int mContainerState = STATE_UNKNOWN; + private final Set<Integer> mFailedModalities = new HashSet<Integer>(); // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet. - @Nullable @AuthDialogCallback.DismissedReason Integer mPendingCallbackReason; + @Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason; // HAT received from LockSettingsService when credential is verified. - @Nullable byte[] mCredentialAttestation; + @Nullable private byte[] mCredentialAttestation; + @VisibleForTesting static class Config { Context mContext; AuthDialogCallback mCallback; @@ -122,11 +124,11 @@ public class AuthContainerView extends LinearLayout int mUserId; String mOpPackageName; int[] mSensorIds; - boolean mCredentialAllowed; boolean mSkipIntro; long mOperationId; long mRequestId; - @BiometricMultiSensorMode int mMultiSensorConfig; + boolean mSkipAnimation = false; + @BiometricMultiSensorMode int mMultiSensorConfig = BIOMETRIC_MULTI_SENSOR_DEFAULT; } public static class Builder { @@ -167,7 +169,7 @@ public class AuthContainerView extends LinearLayout return this; } - public Builder setOperationId(long operationId) { + public Builder setOperationId(@DurationMillisLong long operationId) { mConfig.mOperationId = operationId; return this; } @@ -178,55 +180,27 @@ public class AuthContainerView extends LinearLayout 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 AuthContainerView build(int[] sensorIds, boolean credentialAllowed, + public AuthContainerView build(int[] sensorIds, @Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FaceSensorPropertiesInternal> faceProps, - WakefulnessLifecycle wakefulnessLifecycle) { + @NonNull WakefulnessLifecycle wakefulnessLifecycle, + @NonNull UserManager userManager, + @NonNull LockPatternUtils lockPatternUtils) { mConfig.mSensorIds = sensorIds; - mConfig.mCredentialAllowed = credentialAllowed; - return new AuthContainerView( - mConfig, new Injector(), fpProps, faceProps, wakefulnessLifecycle); - } - } - - public static class Injector { - ScrollView getBiometricScrollView(FrameLayout parent) { - return parent.findViewById(R.id.biometric_scrollview); - } - - FrameLayout inflateContainerView(LayoutInflater factory, ViewGroup root) { - return (FrameLayout) factory.inflate( - R.layout.auth_container_view, root, false /* attachToRoot */); - } - - AuthPanelController getPanelController(Context context, View panelView) { - return new AuthPanelController(context, panelView); - } - - ImageView getBackgroundView(FrameLayout parent) { - return parent.findViewById(R.id.background); - } - - View getPanelView(FrameLayout parent) { - return parent.findViewById(R.id.panel); - } - - int getAnimateCredentialStartDelayMs() { - return AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS; - } - - UserManager getUserManager(Context context) { - return UserManager.get(context); - } - - int getCredentialType(Context context, int effectiveUserId) { - return Utils.getCredentialType(context, effectiveUserId); + return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle, + userManager, lockPatternUtils, new Handler(Looper.getMainLooper())); } } @@ -246,6 +220,7 @@ public class AuthContainerView extends LinearLayout animateAway(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE); break; case AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN: + mFailedModalities.clear(); mConfig.mCallback.onTryAgainPressed(); break; case AuthBiometricView.Callback.ACTION_ERROR: @@ -255,10 +230,7 @@ public class AuthContainerView extends LinearLayout mConfig.mCallback.onDeviceCredentialPressed(); mHandler.postDelayed(() -> { addCredentialView(false /* animatePanel */, true /* animateContents */); - }, mInjector.getAnimateCredentialStartDelayMs()); - break; - case AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR: - mConfig.mCallback.onStartFingerprintNow(); + }, mConfig.mSkipAnimation ? 0 : AuthDialog.ANIMATE_CREDENTIAL_START_DELAY_MS); break; default: Log.e(TAG, "Unhandled action: " + action); @@ -275,21 +247,19 @@ public class AuthContainerView extends LinearLayout } @VisibleForTesting - AuthContainerView(Config config, Injector injector, + AuthContainerView(Config config, @Nullable List<FingerprintSensorPropertiesInternal> fpProps, @Nullable List<FaceSensorPropertiesInternal> faceProps, - WakefulnessLifecycle wakefulnessLifecycle) { + @NonNull WakefulnessLifecycle wakefulnessLifecycle, + @NonNull UserManager userManager, + @NonNull LockPatternUtils lockPatternUtils, + @NonNull Handler mainHandler) { super(config.mContext); mConfig = config; - mInjector = injector; - mFpProps = fpProps; - mFaceProps = faceProps; - - mEffectiveUserId = mInjector.getUserManager(mContext) - .getCredentialOwnerProfile(mConfig.mUserId); - - mHandler = new Handler(Looper.getMainLooper()); + mLockPatternUtils = lockPatternUtils; + mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId); + mHandler = mainHandler; mWindowManager = mContext.getSystemService(WindowManager.class); mWakefulnessLifecycle = wakefulnessLifecycle; @@ -299,100 +269,42 @@ public class AuthContainerView extends LinearLayout mBiometricCallback = new BiometricCallback(); mCredentialCallback = new CredentialCallback(); - final LayoutInflater factory = LayoutInflater.from(mContext); - mFrameLayout = mInjector.inflateContainerView(factory, this); - - mPanelView = mInjector.getPanelView(mFrameLayout); - mPanelController = mInjector.getPanelController(mContext, mPanelView); + final LayoutInflater layoutInflater = LayoutInflater.from(mContext); + mFrameLayout = (FrameLayout) layoutInflater.inflate( + R.layout.auth_container_view, this, false /* attachToRoot */); + addView(mFrameLayout); + mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview); + mBackgroundView = mFrameLayout.findViewById(R.id.background); + mPanelView = mFrameLayout.findViewById(R.id.panel); + mPanelController = new AuthPanelController(mContext, mPanelView); // Inflate biometric view only if necessary. - final int sensorCount = config.mSensorIds.length; if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { - if (sensorCount == 1) { - final int singleSensorAuthId = config.mSensorIds[0]; - if (Utils.containsSensorId(mFpProps, singleSensorAuthId)) { - FingerprintSensorPropertiesInternal sensorProps = null; - for (FingerprintSensorPropertiesInternal prop : mFpProps) { - if (prop.sensorId == singleSensorAuthId) { - sensorProps = prop; - break; - } - } - - if (sensorProps.isAnyUdfpsType()) { - AuthBiometricUdfpsView udfpsView = (AuthBiometricUdfpsView) factory - .inflate(R.layout.auth_biometric_udfps_view, null, false); - udfpsView.setSensorProps(sensorProps); - mBiometricView = udfpsView; - } else { - mBiometricView = (AuthBiometricFingerprintView) factory - .inflate(R.layout.auth_biometric_fingerprint_view, null, false); - } - } else if (Utils.containsSensorId(mFaceProps, singleSensorAuthId)) { - mBiometricView = (AuthBiometricFaceView) - factory.inflate(R.layout.auth_biometric_face_view, null, false); - } else { - // Unknown sensorId - Log.e(TAG, "Unknown sensorId: " + singleSensorAuthId); - mBiometricView = null; - mBackgroundView = null; - mBiometricScrollView = null; - return; - } - } else if (sensorCount == 2) { - final int[] allSensors = findFaceAndFingerprintSensors(); - final int faceSensorId = allSensors[0]; - final int fingerprintSensorId = allSensors[1]; - - if (fingerprintSensorId == -1 || faceSensorId == -1) { - Log.e(TAG, "Missing fingerprint or face for dual-sensor config"); - mBiometricView = null; - mBackgroundView = null; - mBiometricScrollView = null; - return; - } - - FingerprintSensorPropertiesInternal fingerprintSensorProps = null; - for (FingerprintSensorPropertiesInternal prop : mFpProps) { - if (prop.sensorId == fingerprintSensorId) { - fingerprintSensorProps = prop; - break; - } - } - - if (fingerprintSensorProps != null) { - final AuthBiometricFaceToFingerprintView faceToFingerprintView = - (AuthBiometricFaceToFingerprintView) factory.inflate( - R.layout.auth_biometric_face_to_fingerprint_view, null, false); - faceToFingerprintView.setFingerprintSensorProps(fingerprintSensorProps); - faceToFingerprintView.setModalityListener(new ModalityListener() { - @Override - public void onModalitySwitched(int oldModality, int newModality) { - maybeUpdatePositionForUdfps(true /* invalidate */); - } - }); - mBiometricView = faceToFingerprintView; - } else { - Log.e(TAG, "Fingerprint props not found for sensor ID: " + fingerprintSensorId); - mBiometricView = null; - mBackgroundView = null; - mBiometricScrollView = null; - return; - } + final FingerprintSensorPropertiesInternal fpProperties = + Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds); + final FaceSensorPropertiesInternal faceProperties = + Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds); + + if (fpProperties != null && faceProperties != null) { + final AuthBiometricFingerprintAndFaceView fingerprintAndFaceView = + (AuthBiometricFingerprintAndFaceView) layoutInflater.inflate( + R.layout.auth_biometric_fingerprint_and_face_view, null, false); + fingerprintAndFaceView.setSensorProperties(fpProperties); + mBiometricView = fingerprintAndFaceView; + } else if (fpProperties != null) { + final AuthBiometricFingerprintView fpView = + (AuthBiometricFingerprintView) layoutInflater.inflate( + R.layout.auth_biometric_fingerprint_view, null, false); + fpView.setSensorProperties(fpProperties); + mBiometricView = fpView; + } else if (faceProperties != null) { + mBiometricView = (AuthBiometricFaceView) layoutInflater.inflate( + R.layout.auth_biometric_face_view, null, false); } else { - Log.e(TAG, "Unsupported sensor array, length: " + sensorCount); - mBiometricView = null; - mBackgroundView = null; - mBiometricScrollView = null; - return; + Log.e(TAG, "No sensors found!"); } } - mBiometricScrollView = mInjector.getBiometricScrollView(mFrameLayout); - mBackgroundView = mInjector.getBackgroundView(mFrameLayout); - - addView(mFrameLayout); - // init view before showing if (mBiometricView != null) { mBiometricView.setRequireConfirmation(mConfig.mRequireConfirmation); @@ -431,10 +343,6 @@ public class AuthContainerView extends LinearLayout return Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo); } - private void addBiometricView() { - mBiometricScrollView.addView(mBiometricView); - } - /** * Adds the credential view. When going from biometric to credential view, the biometric * view starts the panel expansion animation. If the credential view is being shown first, @@ -444,8 +352,8 @@ public class AuthContainerView extends LinearLayout private void addCredentialView(boolean animatePanel, boolean animateContents) { final LayoutInflater factory = LayoutInflater.from(mContext); - final @Utils.CredentialType int credentialType = mInjector.getCredentialType( - mContext, mEffectiveUserId); + @Utils.CredentialType final int credentialType = Utils.getCredentialType( + mLockPatternUtils, mEffectiveUserId); switch (credentialType) { case Utils.CREDENTIAL_PATTERN: @@ -493,15 +401,11 @@ public class AuthContainerView extends LinearLayout @Override public void onAttachedToWindow() { super.onAttachedToWindow(); - onAttachedToWindowInternal(); - } - @VisibleForTesting - void onAttachedToWindowInternal() { mWakefulnessLifecycle.addObserver(this); if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { - addBiometricView(); + mBiometricScrollView.addView(mBiometricView); } else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) { addCredentialView(true /* animatePanel */, false /* animateContents */); } else { @@ -521,17 +425,18 @@ public class AuthContainerView extends LinearLayout mBiometricScrollView.setY(mTranslationY); setAlpha(0f); + final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS; postOnAnimation(() -> { mPanelView.animate() .translationY(0) - .setDuration(ANIMATION_DURATION_SHOW_MS) + .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .withLayer() .withEndAction(this::onDialogAnimatedIn) .start(); mBiometricScrollView.animate() .translationY(0) - .setDuration(ANIMATION_DURATION_SHOW_MS) + .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .withLayer() .start(); @@ -539,14 +444,14 @@ public class AuthContainerView extends LinearLayout mCredentialView.setY(mTranslationY); mCredentialView.animate() .translationY(0) - .setDuration(ANIMATION_DURATION_SHOW_MS) + .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .withLayer() .start(); } animate() .alpha(1f) - .setDuration(ANIMATION_DURATION_SHOW_MS) + .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .withLayer() .start(); @@ -555,15 +460,8 @@ public class AuthContainerView extends LinearLayout } private static boolean shouldUpdatePositionForUdfps(@NonNull View view) { - if (view instanceof AuthBiometricUdfpsView) { - return true; - } - - if (view instanceof AuthBiometricFaceToFingerprintView) { - AuthBiometricFaceToFingerprintView faceToFingerprintView = - (AuthBiometricFaceToFingerprintView) view; - return faceToFingerprintView.getActiveSensorType() == TYPE_FINGERPRINT - && faceToFingerprintView.isFingerprintUdfps(); + if (view instanceof AuthBiometricFingerprintView) { + return ((AuthBiometricFingerprintView) view).isUdfps(); } return false; @@ -652,12 +550,13 @@ public class AuthContainerView extends LinearLayout } @Override - public void onAuthenticationSucceeded() { - mBiometricView.onAuthenticationSucceeded(); + public void onAuthenticationSucceeded(@Modality int modality) { + mBiometricView.onAuthenticationSucceeded(modality); } @Override public void onAuthenticationFailed(@Modality int modality, String failureReason) { + mFailedModalities.add(modality); mBiometricView.onAuthenticationFailed(modality, failureReason); } @@ -672,8 +571,17 @@ public class AuthContainerView extends LinearLayout } @Override + public void onPointerDown() { + if (mBiometricView.onPointerDown(mFailedModalities)) { + Log.d(TAG, "retrying failed modalities (pointer down)"); + mBiometricCallback.onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN); + } + } + + @Override public void onSaveState(@NonNull Bundle outState) { - outState.putInt(AuthDialog.KEY_CONTAINER_STATE, mContainerState); + outState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, + mContainerState == STATE_ANIMATING_OUT); // In the case where biometric and credential are both allowed, we can assume that // biometric isn't showing if credential is showing since biometric is shown first. outState.putBoolean(AuthDialog.KEY_BIOMETRIC_SHOWING, @@ -695,8 +603,7 @@ public class AuthContainerView extends LinearLayout mBiometricView.startTransitionToCredentialUI(); } - @VisibleForTesting - void animateAway(int reason) { + void animateAway(@AuthDialogCallback.DismissedReason int reason) { animateAway(true /* sendReason */, reason); } @@ -724,31 +631,32 @@ public class AuthContainerView extends LinearLayout removeWindowIfAttached(); }; + final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS; postOnAnimation(() -> { mPanelView.animate() .translationY(mTranslationY) - .setDuration(ANIMATION_DURATION_AWAY_MS) + .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .withLayer() .withEndAction(endActionRunnable) .start(); mBiometricScrollView.animate() .translationY(mTranslationY) - .setDuration(ANIMATION_DURATION_AWAY_MS) + .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .withLayer() .start(); if (mCredentialView != null && mCredentialView.isAttachedToWindow()) { mCredentialView.animate() .translationY(mTranslationY) - .setDuration(ANIMATION_DURATION_AWAY_MS) + .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .withLayer() .start(); } animate() .alpha(0f) - .setDuration(ANIMATION_DURATION_AWAY_MS) + .setDuration(animateDuration) .setInterpolator(mLinearOutSlowIn) .withLayer() .start(); @@ -773,8 +681,7 @@ public class AuthContainerView extends LinearLayout mWindowManager.removeView(this); } - @VisibleForTesting - void onDialogAnimatedIn() { + private void onDialogAnimatedIn() { if (mContainerState == STATE_PENDING_DISMISS) { Log.d(TAG, "onDialogAnimatedIn(): mPendingDismissDialog=true, dismissing now"); animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); @@ -788,8 +695,7 @@ public class AuthContainerView extends LinearLayout } @VisibleForTesting - static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, - CharSequence title) { + static WindowManager.LayoutParams getLayoutParams(IBinder windowToken, CharSequence title) { final int windowFlags = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_SECURE; final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( @@ -805,24 +711,4 @@ public class AuthContainerView extends LinearLayout lp.token = windowToken; return lp; } - - // returns [face, fingerprint] sensor ids (id is -1 if not present) - private int[] findFaceAndFingerprintSensors() { - int faceSensorId = -1; - int fingerprintSensorId = -1; - - for (final int sensorId : mConfig.mSensorIds) { - if (Utils.containsSensorId(mFpProps, sensorId)) { - fingerprintSensorId = sensorId; - } else if (Utils.containsSensorId(mFaceProps, sensorId)) { - faceSensorId = sensorId; - } - - if (fingerprintSensorId != -1 && faceSensorId != -1) { - break; - } - } - - return new int[] {faceSensorId, fingerprintSensorId}; - } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index dfb8c18b4ece..64c2d2e3858e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -51,6 +51,7 @@ import android.hardware.fingerprint.IUdfpsHbmListener; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; +import android.os.UserManager; import android.util.Log; import android.util.SparseBooleanArray; import android.view.MotionEvent; @@ -59,6 +60,7 @@ import android.view.WindowManager; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; +import com.android.internal.widget.LockPatternUtils; import com.android.systemui.CoreStartable; import com.android.systemui.assist.ui.DisplayUtils; import com.android.systemui.dagger.SysUISingleton; @@ -123,8 +125,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba @Nullable private SidefpsController mSidefpsController; @Nullable private IBiometricContextListener mBiometricContextListener; @VisibleForTesting - TaskStackListener mTaskStackListener; - @VisibleForTesting IBiometricSysuiReceiver mReceiver; @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener; @@ -137,13 +137,16 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba @NonNull private final SensorPrivacyManager mSensorPrivacyManager; private final WakefulnessLifecycle mWakefulnessLifecycle; private boolean mAllAuthenticatorsRegistered; + @NonNull private final UserManager mUserManager; + @NonNull private final LockPatternUtils mLockPatternUtils; - private class BiometricTaskStackListener extends TaskStackListener { + @VisibleForTesting + final TaskStackListener mTaskStackListener = new TaskStackListener() { @Override public void onTaskStackChanged() { mHandler.post(AuthController.this::handleTaskStackChanged); } - } + }; private final IFingerprintAuthenticatorsRegisteredCallback mFingerprintAuthenticatorsRegisteredCallback = @@ -256,6 +259,17 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba mUdfpsProps = !udfpsProps.isEmpty() ? udfpsProps : null; if (mUdfpsProps != null) { mUdfpsController = mUdfpsControllerFactory.get(); + mUdfpsController.addCallback(new UdfpsController.Callback() { + @Override + public void onFingerUp() {} + + @Override + public void onFingerDown() { + if (mCurrentDialog != null) { + mCurrentDialog.onPointerDown(); + } + } + }); } mSidefpsProps = !sidefpsProps.isEmpty() ? sidefpsProps : null; if (mSidefpsProps != null) { @@ -360,20 +374,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba } @Override - public void onStartFingerprintNow() { - if (mReceiver == null) { - Log.e(TAG, "onStartUdfpsNow: Receiver is null"); - return; - } - - try { - mReceiver.onStartFingerprintNow(); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException when sending onDialogAnimatedIn", e); - } - } - - @Override public void onDismissed(@DismissedReason int reason, @Nullable byte[] credentialAttestation) { switch (reason) { case AuthDialogCallback.DISMISSED_USER_CANCELED: @@ -503,12 +503,16 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba Provider<UdfpsController> udfpsControllerFactory, Provider<SidefpsController> sidefpsControllerFactory, @NonNull DisplayManager displayManager, - WakefulnessLifecycle wakefulnessLifecycle, + @NonNull WakefulnessLifecycle wakefulnessLifecycle, + @NonNull UserManager userManager, + @NonNull LockPatternUtils lockPatternUtils, @NonNull StatusBarStateController statusBarStateController, @Main Handler handler) { super(context); mExecution = execution; mWakefulnessLifecycle = wakefulnessLifecycle; + mUserManager = userManager; + mLockPatternUtils = lockPatternUtils; mHandler = handler; mCommandQueue = commandQueue; mActivityTaskManager = activityTaskManager; @@ -583,7 +587,6 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba mFingerprintAuthenticatorsRegisteredCallback); } - mTaskStackListener = new BiometricTaskStackListener(); mActivityTaskManager.registerTaskStackListener(mTaskStackListener); } @@ -668,11 +671,11 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba * example, KeyguardUpdateMonitor has its own {@link FingerprintManager.AuthenticationCallback}. */ @Override - public void onBiometricAuthenticated() { + public void onBiometricAuthenticated(@Modality int modality) { if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: "); if (mCurrentDialog != null) { - mCurrentDialog.onAuthenticationSucceeded(); + mCurrentDialog.onAuthenticationSucceeded(modality); } else { Log.w(TAG, "onBiometricAuthenticated callback but dialog gone"); } @@ -827,7 +830,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba final String opPackageName = (String) args.arg6; final long operationId = args.argl1; final long requestId = args.argl2; - final @BiometricMultiSensorMode int multiSensorConfig = args.argi2; + @BiometricMultiSensorMode final int multiSensorConfig = args.argi2; // Create a new dialog but do not replace the current one yet. final AuthDialog newDialog = buildDialog( @@ -835,13 +838,14 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba requireConfirmation, userId, sensorIds, - credentialAllowed, opPackageName, skipAnimation, operationId, requestId, multiSensorConfig, - mWakefulnessLifecycle); + mWakefulnessLifecycle, + mUserManager, + mLockPatternUtils); if (newDialog == null) { Log.e(TAG, "Unsupported type configuration"); @@ -902,8 +906,7 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba // Only show the dialog if necessary. If it was animating out, the dialog is supposed // to send its pending callback immediately. - if (savedState.getInt(AuthDialog.KEY_CONTAINER_STATE) - != AuthContainerView.STATE_ANIMATING_OUT) { + if (!savedState.getBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false)) { final boolean credentialShowing = savedState.getBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING); if (credentialShowing) { @@ -927,10 +930,12 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba } protected AuthDialog buildDialog(PromptInfo promptInfo, boolean requireConfirmation, - int userId, int[] sensorIds, boolean credentialAllowed, String opPackageName, + int userId, int[] sensorIds, String opPackageName, boolean skipIntro, long operationId, long requestId, @BiometricMultiSensorMode int multiSensorConfig, - WakefulnessLifecycle wakefulnessLifecycle) { + @NonNull WakefulnessLifecycle wakefulnessLifecycle, + @NonNull UserManager userManager, + @NonNull LockPatternUtils lockPatternUtils) { return new AuthContainerView.Builder(mContext) .setCallback(this) .setPromptInfo(promptInfo) @@ -941,7 +946,8 @@ public class AuthController extends CoreStartable implements CommandQueue.Callba .setOperationId(operationId) .setRequestId(requestId) .setMultiSensorConfig(multiSensorConfig) - .build(sensorIds, credentialAllowed, mFpProps, mFaceProps, wakefulnessLifecycle); + .build(sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle, userManager, + lockPatternUtils); } /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java index fa5213e94081..59ed156bce33 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java @@ -31,7 +31,7 @@ import java.lang.annotation.RetentionPolicy; */ public interface AuthDialog { - String KEY_CONTAINER_STATE = "container_state"; + String KEY_CONTAINER_GOING_AWAY = "container_going_away"; String KEY_BIOMETRIC_SHOWING = "biometric_showing"; String KEY_CREDENTIAL_SHOWING = "credential_showing"; @@ -64,7 +64,7 @@ public interface AuthDialog { @interface DialogSize {} /** - * Parameters used when laying out {@link AuthBiometricView}, its sublclasses, and + * Parameters used when laying out {@link AuthBiometricView}, its subclasses, and * {@link AuthPanelController}. */ class LayoutParams { @@ -113,7 +113,7 @@ public interface AuthDialog { /** * Biometric authenticated. May be pending user confirmation, or completed. */ - void onAuthenticationSucceeded(); + void onAuthenticationSucceeded(@Modality int modality); /** * Authentication failed (reject, timeout). Dialog stays showing. @@ -136,6 +136,9 @@ public interface AuthDialog { */ void onError(@Modality int modality, String error); + /** UDFPS pointer down event. */ + void onPointerDown(); + /** * Save the current state. * @param outState diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java index 9f40ca7b0346..a7d2901b21c3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java @@ -70,9 +70,4 @@ public interface AuthDialogCallback { * Notifies when the dialog has finished animating. */ void onDialogAnimatedIn(); - - /** - * Notifies that the fingerprint sensor should be started now. - */ - void onStartFingerprintNow(); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.kt index 6607915fac9d..242601d46fa4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.kt @@ -23,7 +23,7 @@ import android.util.AttributeSet * * Currently doesn't draw anything. * - * Note that [AuthBiometricUdfpsView] also shows UDFPS animations. At some point we should + * Note that [AuthBiometricFingerprintViewController] also shows UDFPS animations. At some point we should * de-dupe this if necessary. */ class UdfpsBpView(context: Context, attrs: AttributeSet?) : UdfpsAnimationView(context, attrs) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 590963b2ff48..086894d2e670 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -300,7 +300,10 @@ class UdfpsControllerOverlay( when (context.display!!.rotation) { Surface.ROTATION_90 -> { if (!shouldRotate(animation)) { - Log.v(TAG, "skip rotating udfps location ROTATION_90") + Log.v(TAG, "skip rotating udfps location ROTATION_90" + + " animation=$animation" + + " isGoingToSleep=${keyguardUpdateMonitor.isGoingToSleep}" + + " isOccluded=${keyguardStateController.isOccluded}") } else { Log.v(TAG, "rotate udfps location ROTATION_90") x = (location.sensorLocationY - location.sensorRadius - paddingX) @@ -309,7 +312,10 @@ class UdfpsControllerOverlay( } Surface.ROTATION_270 -> { if (!shouldRotate(animation)) { - Log.v(TAG, "skip rotating udfps location ROTATION_270") + Log.v(TAG, "skip rotating udfps location ROTATION_270" + + " animation=$animation" + + " isGoingToSleep=${keyguardUpdateMonitor.isGoingToSleep}" + + " isOccluded=${keyguardStateController.isOccluded}") } else { Log.v(TAG, "rotate udfps location ROTATION_270") x = (p.x - location.sensorLocationY - location.sensorRadius - paddingX) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java deleted file mode 100644 index 6989547dce52..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; -import static android.hardware.biometrics.BiometricManager.Authenticators; -import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.admin.DevicePolicyManager; -import android.content.Context; -import android.content.pm.PackageManager; -import android.hardware.biometrics.PromptInfo; -import android.hardware.biometrics.SensorPropertiesInternal; -import android.os.UserManager; -import android.util.DisplayMetrics; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; - -import com.android.internal.widget.LockPatternUtils; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.List; - -public class Utils { - - public static final int CREDENTIAL_PIN = 1; - public static final int CREDENTIAL_PATTERN = 2; - public static final int CREDENTIAL_PASSWORD = 3; - - /** Base set of layout flags for fingerprint overlay widgets. */ - public static final int FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS = - WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD}) - @interface CredentialType {} - - static float dpToPixels(Context context, float dp) { - return dp * ((float) context.getResources().getDisplayMetrics().densityDpi - / DisplayMetrics.DENSITY_DEFAULT); - } - - static void notifyAccessibilityContentChanged(AccessibilityManager am, ViewGroup view) { - if (!am.isEnabled()) { - return; - } - AccessibilityEvent event = AccessibilityEvent.obtain(); - event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - event.setContentChangeTypes(CONTENT_CHANGE_TYPE_SUBTREE); - view.sendAccessibilityEventUnchecked(event); - view.notifySubtreeAccessibilityStateChanged(view, view, CONTENT_CHANGE_TYPE_SUBTREE); - } - - static boolean isDeviceCredentialAllowed(PromptInfo promptInfo) { - @Authenticators.Types final int authenticators = promptInfo.getAuthenticators(); - return (authenticators & Authenticators.DEVICE_CREDENTIAL) != 0; - } - - static boolean isBiometricAllowed(PromptInfo promptInfo) { - @Authenticators.Types final int authenticators = promptInfo.getAuthenticators(); - return (authenticators & Authenticators.BIOMETRIC_WEAK) != 0; - } - - static @CredentialType int getCredentialType(Context context, int userId) { - final LockPatternUtils lpu = new LockPatternUtils(context); - switch (lpu.getKeyguardStoredPasswordQuality(userId)) { - case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: - return CREDENTIAL_PATTERN; - case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: - case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: - return CREDENTIAL_PIN; - case DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC: - case DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC: - case DevicePolicyManager.PASSWORD_QUALITY_COMPLEX: - case DevicePolicyManager.PASSWORD_QUALITY_MANAGED: - return CREDENTIAL_PASSWORD; - default: - return CREDENTIAL_PASSWORD; - } - } - - static boolean isManagedProfile(Context context, int userId) { - final UserManager userManager = context.getSystemService(UserManager.class); - return userManager.isManagedProfile(userId); - } - - static boolean containsSensorId(@Nullable List<? extends SensorPropertiesInternal> properties, - int sensorId) { - if (properties == null) { - return false; - } - - for (SensorPropertiesInternal prop : properties) { - if (prop.sensorId == sensorId) { - return true; - } - } - - return false; - } - - static boolean isSystem(@NonNull Context context, @Nullable String clientPackage) { - final boolean hasPermission = context.checkCallingOrSelfPermission(USE_BIOMETRIC_INTERNAL) - == PackageManager.PERMISSION_GRANTED; - return hasPermission && "android".equals(clientPackage); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt new file mode 100644 index 000000000000..d0d6f4cbf166 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.biometrics + +import android.Manifest +import android.annotation.IntDef +import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC +import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC +import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX +import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED +import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC +import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX +import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING +import android.content.Context +import android.content.pm.PackageManager +import android.hardware.biometrics.BiometricManager.Authenticators +import android.hardware.biometrics.PromptInfo +import android.hardware.biometrics.SensorPropertiesInternal +import android.os.UserManager +import android.util.DisplayMetrics +import android.view.ViewGroup +import android.view.WindowManager +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityManager +import com.android.internal.widget.LockPatternUtils +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy + +object Utils { + const val CREDENTIAL_PIN = 1 + const val CREDENTIAL_PATTERN = 2 + const val CREDENTIAL_PASSWORD = 3 + + /** Base set of layout flags for fingerprint overlay widgets. */ + const val FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS = + (WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) + + @JvmStatic + fun dpToPixels(context: Context, dp: Float): Float { + val density = context.resources.displayMetrics.densityDpi.toFloat() + return dp * (density / DisplayMetrics.DENSITY_DEFAULT) + } + + @JvmStatic + fun notifyAccessibilityContentChanged(am: AccessibilityManager, view: ViewGroup) { + if (!am.isEnabled) { + return + } + val event = AccessibilityEvent.obtain() + event.eventType = AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED + event.contentChangeTypes = + AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE + view.sendAccessibilityEventUnchecked(event) + view.notifySubtreeAccessibilityStateChanged( + view, + view, + AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE + ) + } + + @JvmStatic + fun isDeviceCredentialAllowed(promptInfo: PromptInfo): Boolean = + (promptInfo.authenticators and Authenticators.DEVICE_CREDENTIAL) != 0 + + @JvmStatic + fun isBiometricAllowed(promptInfo: PromptInfo): Boolean = + (promptInfo.authenticators and Authenticators.BIOMETRIC_WEAK) != 0 + + @JvmStatic + @CredentialType + fun getCredentialType(utils: LockPatternUtils, userId: Int): Int = + when (utils.getKeyguardStoredPasswordQuality(userId)) { + PASSWORD_QUALITY_SOMETHING -> CREDENTIAL_PATTERN + PASSWORD_QUALITY_NUMERIC, + PASSWORD_QUALITY_NUMERIC_COMPLEX -> CREDENTIAL_PIN + PASSWORD_QUALITY_ALPHABETIC, + PASSWORD_QUALITY_ALPHANUMERIC, + PASSWORD_QUALITY_COMPLEX, + PASSWORD_QUALITY_MANAGED -> CREDENTIAL_PASSWORD + else -> CREDENTIAL_PASSWORD + } + + @JvmStatic + fun isManagedProfile(context: Context, userId: Int): Boolean = + context.getSystemService(UserManager::class.java)?.isManagedProfile(userId) ?: false + + @JvmStatic + fun <T : SensorPropertiesInternal> findFirstSensorProperties( + properties: List<T>?, + sensorIds: IntArray + ): T? = properties?.firstOrNull { sensorIds.contains(it.sensorId) } + + @JvmStatic + fun isSystem(context: Context, clientPackage: String?): Boolean { + val hasPermission = + (context.checkCallingOrSelfPermission(Manifest.permission.USE_BIOMETRIC_INTERNAL) + == PackageManager.PERMISSION_GRANTED) + return hasPermission && "android" == clientPackage + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef(CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD) + internal annotation class CredentialType +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt index 4819bf565cbc..a4f9f3a9bc08 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt @@ -54,7 +54,7 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.controls.ControlsMetricsLogger import com.android.systemui.controls.controller.ControlsController import com.android.systemui.util.concurrency.DelayableExecutor -import kotlin.reflect.KClass +import java.util.function.Supplier /** * Wraps the widgets that make up the UI representation of a {@link Control}. Updates to the view @@ -90,20 +90,20 @@ class ControlViewHolder( status: Int, template: ControlTemplate, deviceType: Int - ): KClass<out Behavior> { + ): Supplier<out Behavior> { return when { - status != Control.STATUS_OK -> StatusBehavior::class - template == ControlTemplate.NO_TEMPLATE -> TouchBehavior::class - template is ThumbnailTemplate -> ThumbnailBehavior::class + status != Control.STATUS_OK -> Supplier { StatusBehavior() } + template == ControlTemplate.NO_TEMPLATE -> Supplier { TouchBehavior() } + template is ThumbnailTemplate -> Supplier { ThumbnailBehavior() } // Required for legacy support, or where cameras do not use the new template - deviceType == DeviceTypes.TYPE_CAMERA -> TouchBehavior::class - template is ToggleTemplate -> ToggleBehavior::class - template is StatelessTemplate -> TouchBehavior::class - template is ToggleRangeTemplate -> ToggleRangeBehavior::class - template is RangeTemplate -> ToggleRangeBehavior::class - template is TemperatureControlTemplate -> TemperatureControlBehavior::class - else -> DefaultBehavior::class + deviceType == DeviceTypes.TYPE_CAMERA -> Supplier { TouchBehavior() } + template is ToggleTemplate -> Supplier { ToggleBehavior() } + template is StatelessTemplate -> Supplier { TouchBehavior() } + template is ToggleRangeTemplate -> Supplier { ToggleRangeBehavior() } + template is RangeTemplate -> Supplier { ToggleRangeBehavior() } + template is TemperatureControlTemplate -> Supplier { TemperatureControlBehavior() } + else -> Supplier { DefaultBehavior() } } } } @@ -253,13 +253,14 @@ class ControlViewHolder( fun bindBehavior( existingBehavior: Behavior?, - clazz: KClass<out Behavior>, + supplier: Supplier<out Behavior>, offset: Int = 0 ): Behavior { - val behavior = if (existingBehavior == null || existingBehavior!!::class != clazz) { + val newBehavior = supplier.get() + val behavior = if (existingBehavior == null || + existingBehavior::class != newBehavior::class) { // Behavior changes can signal a change in template from the app or // first time setup - val newBehavior = clazz.java.newInstance() newBehavior.initialize(this) // let behaviors define their own, if necessary, and clear any existing ones diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 7e1fce298fbd..ebc766635733 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -19,6 +19,8 @@ package com.android.systemui.dreams; import android.content.Context; import android.graphics.drawable.ColorDrawable; import android.util.Log; +import android.view.View; +import android.view.ViewGroup; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; @@ -177,9 +179,26 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ } mDreamOverlayContainerViewController.init(); + // Make extra sure the container view has been removed from its old parent (otherwise we + // risk an IllegalStateException in some cases when setting the container view as the + // window's content view and the container view hasn't been properly removed previously). + removeContainerViewFromParent(); mWindow.setContentView(mDreamOverlayContainerViewController.getContainerView()); final WindowManager windowManager = mContext.getSystemService(WindowManager.class); windowManager.addView(mWindow.getDecorView(), mWindow.getAttributes()); } + + private void removeContainerViewFromParent() { + View containerView = mDreamOverlayContainerViewController.getContainerView(); + if (containerView == null) { + return; + } + ViewGroup parentView = (ViewGroup) containerView.getParent(); + if (parentView == null) { + return; + } + Log.w(TAG, "Removing dream overlay container view parent!"); + parentView.removeView(containerView); + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 582965a12528..35f29b94966f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -23,6 +23,7 @@ import android.content.Context import android.graphics.Matrix import android.graphics.Rect import android.os.Handler +import android.util.Log import android.view.RemoteAnimationTarget import android.view.SyncRtSurfaceTransactionApplier import android.view.View @@ -47,6 +48,8 @@ import dagger.Lazy import javax.inject.Inject import kotlin.math.min +const val TAG = "KeyguardUnlock" + /** * Starting scale factor for the app/launcher surface behind the keyguard, when it's animating * in during keyguard exit. @@ -584,8 +587,16 @@ class KeyguardUnlockAnimationController @Inject constructor( * animation. */ fun hideKeyguardViewAfterRemoteAnimation() { - // Hide the keyguard, with no fade out since we animated it away during the unlock. - keyguardViewController.hide(surfaceBehindRemoteAnimationStartTime, 0 /* fadeOutDuration */) + if (keyguardViewController.isShowing) { + // Hide the keyguard, with no fade out since we animated it away during the unlock. + keyguardViewController.hide( + surfaceBehindRemoteAnimationStartTime, + 0 /* fadeOutDuration */ + ) + } else { + Log.e(TAG, "#hideKeyguardViewAfterRemoteAnimation called when keyguard view is not " + + "showing. Ignoring...") + } } private fun applyParamsToSurface(params: SyncRtSurfaceTransactionApplier.SurfaceParams) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index 3cd390533132..0b23ad50a726 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -54,6 +54,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { MediaOutputDialog mediaOutputDialog) { super(controller); mMediaOutputDialog = mediaOutputDialog; + setHasStableIds(true); } @Override @@ -79,6 +80,20 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } @Override + public long getItemId(int position) { + final int size = mController.getMediaDevices().size(); + if (position == size && mController.isZeroMode()) { + return -1; + } else if (position < size) { + return ((List<MediaDevice>) (mController.getMediaDevices())) + .get(position).getId().hashCode(); + } else if (DEBUG) { + Log.d(TAG, "Incorrect position for item id: " + position); + } + return position; + } + + @Override public int getItemCount() { if (mController.isZeroMode()) { // Add extra one for "pair new" or dynamic group @@ -159,7 +174,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { onCheckBoxClicked(false, device); }); setCheckBoxColor(mCheckBox, mController.getColorActiveItem()); - initSessionSeekbar(); + initSeekbar(device); } else if (!mController.hasAdjustVolumeUserRestriction() && currentlyConnected) { mStatusIcon.setImageDrawable( mContext.getDrawable(R.drawable.media_output_status_check)); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 1f11d0c62bd7..c96aca37987b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -249,7 +249,7 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar.setMin(0); final int currentVolume = device.getCurrentVolume(); if (mSeekBar.getProgress() != currentVolume) { - mSeekBar.setProgress(currentVolume); + mSeekBar.setProgress(currentVolume, true); } mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override @@ -278,7 +278,7 @@ public abstract class MediaOutputBaseAdapter extends mSeekBar.setMin(0); final int currentVolume = mController.getSessionVolume(); if (mSeekBar.getProgress() != currentVolume) { - mSeekBar.setProgress(currentVolume); + mSeekBar.setProgress(currentVolume, true); } mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override 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 9ea27634df6a..4dacf5dfd78c 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -263,6 +263,15 @@ public class EdgeBackGestureHandler extends CurrentUserTracker // Notify FalsingManager that an intentional gesture has occurred. // TODO(b/186519446): use a different method than isFalseTouch mFalsingManager.isFalseTouch(BACK_GESTURE); + // Only inject back keycodes when ahead-of-time back dispatching is disabled. + if (mBackAnimation == null) { + boolean sendDown = sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); + boolean sendUp = sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK); + if (DEBUG_MISSING_GESTURE) { + Log.d(DEBUG_MISSING_GESTURE_TAG, "Triggered back: down=" + + sendDown + ", up=" + sendUp); + } + } mOverviewProxyService.notifyBackAction(true, (int) mDownPoint.x, (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge); @@ -936,6 +945,9 @@ public class EdgeBackGestureHandler extends CurrentUserTracker public void setBackAnimation(BackAnimation backAnimation) { mBackAnimation = backAnimation; + if (mEdgeBackPlugin != null && mEdgeBackPlugin instanceof NavigationBarEdgePanel) { + ((NavigationBarEdgePanel) mEdgeBackPlugin).setBackAnimation(backAnimation); + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java index a6bad15e0865..a6919e826d4f 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java @@ -280,7 +280,7 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl } }; private BackCallback mBackCallback; - private final BackAnimation mBackAnimation; + private BackAnimation mBackAnimation; public NavigationBarEdgePanel(Context context, BackAnimation backAnimation) { @@ -385,6 +385,10 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl mShowProtection = !isPrimaryDisplay; } + public void setBackAnimation(BackAnimation backAnimation) { + mBackAnimation = backAnimation; + } + @Override public void onDestroy() { cancelFailsafe(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 8a02e5952659..5932a64c1c71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -310,11 +310,11 @@ public class CommandQueue extends IStatusBar.Stub implements long requestId, @BiometricMultiSensorMode int multiSensorConfig) { } - /** @see IStatusBar#onBiometricAuthenticated() */ - default void onBiometricAuthenticated() { + /** @see IStatusBar#onBiometricAuthenticated(int) */ + default void onBiometricAuthenticated(@Modality int modality) { } - /** @see IStatusBar#onBiometricHelp(String) */ + /** @see IStatusBar#onBiometricHelp(int, String) */ default void onBiometricHelp(@Modality int modality, String message) { } @@ -963,9 +963,11 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override - public void onBiometricAuthenticated() { + public void onBiometricAuthenticated(@Modality int modality) { synchronized (mLock) { - mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED).sendToTarget(); + SomeArgs args = SomeArgs.obtain(); + args.argi1 = modality; + mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATED, args).sendToTarget(); } } @@ -1465,9 +1467,11 @@ public class CommandQueue extends IStatusBar.Stub implements break; } case MSG_BIOMETRIC_AUTHENTICATED: { + SomeArgs someArgs = (SomeArgs) msg.obj; for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).onBiometricAuthenticated(); + mCallbacks.get(i).onBiometricAuthenticated(someArgs.argi1 /* modality */); } + someArgs.recycle(); break; } case MSG_BIOMETRIC_HELP: { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 0509a7caa719..ccec0c2d58cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -50,6 +50,7 @@ import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; import android.os.BatteryManager; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.UserHandle; @@ -122,7 +123,7 @@ public class KeyguardIndicationController { private final Context mContext; private final BroadcastDispatcher mBroadcastDispatcher; private final KeyguardStateController mKeyguardStateController; - private final StatusBarStateController mStatusBarStateController; + protected final StatusBarStateController mStatusBarStateController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private ViewGroup mIndicationArea; private KeyguardIndicationTextView mTopIndicationView; @@ -138,6 +139,7 @@ public class KeyguardIndicationController { private final IActivityManager mIActivityManager; private final FalsingManager mFalsingManager; private final KeyguardBypassController mKeyguardBypassController; + private final Handler mHandler; protected KeyguardIndicationRotateTextViewController mRotateTextViewController; private BroadcastReceiver mBroadcastReceiver; @@ -194,7 +196,9 @@ public class KeyguardIndicationController { * Creates a new KeyguardIndicationController and registers callbacks. */ @Inject - public KeyguardIndicationController(Context context, + public KeyguardIndicationController( + Context context, + @Main Looper mainLooper, WakeLock.Builder wakeLockBuilder, KeyguardStateController keyguardStateController, StatusBarStateController statusBarStateController, @@ -230,6 +234,19 @@ public class KeyguardIndicationController { mKeyguardBypassController = keyguardBypassController; mScreenLifecycle = screenLifecycle; mScreenLifecycle.addObserver(mScreenObserver); + + mHandler = new Handler(mainLooper) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_HIDE_TRANSIENT) { + hideTransientIndication(); + } else if (msg.what == MSG_SHOW_ACTION_TO_UNLOCK) { + showActionToUnlock(); + } else if (msg.what == MSG_HIDE_BIOMETRIC_MESSAGE) { + hideBiometricMessage(); + } + } + }; } /** Call this after construction to finish setting up the instance. */ @@ -242,7 +259,6 @@ public class KeyguardIndicationController { mDockManager.addAlignmentStateListener( alignState -> mHandler.post(() -> handleAlignStateChanged(alignState))); mKeyguardUpdateMonitor.registerCallback(getKeyguardCallback()); - mKeyguardUpdateMonitor.registerCallback(mTickReceiver); mStatusBarStateController.addCallback(mStatusBarStateListener); mKeyguardStateController.addCallback(mKeyguardStateCallback); @@ -260,7 +276,7 @@ public class KeyguardIndicationController { mLockScreenIndicationView, mExecutor, mStatusBarStateController); - updateIndication(false /* animate */); + updateDeviceEntryIndication(false /* animate */); updateOrganizedOwnedDevice(); if (mBroadcastReceiver == null) { // Update the disclosure proactively to avoid IPC on the critical path. @@ -288,7 +304,7 @@ public class KeyguardIndicationController { } if (!alignmentIndication.equals(mAlignmentIndication)) { mAlignmentIndication = alignmentIndication; - updateIndication(false); + updateDeviceEntryIndication(false); } } @@ -309,28 +325,30 @@ public class KeyguardIndicationController { return mUpdateMonitorCallback; } - /** - * This method also doesn't update transient messages like biometrics since those messages - * are also updated separately. - */ - private void updatePersistentIndications(boolean animate, int userId) { - updateDisclosure(); - updateOwnerInfo(); - updateBattery(animate); - updateUserLocked(userId); - updateTrust(userId, getTrustGrantedIndication(), getTrustManagedIndication()); - updateAlignment(); - updateLogoutView(); - updateResting(); + private void updateLockScreenIndications(boolean animate, int userId) { + // update transient messages: + updateBiometricMessage(); + updateTransient(); + + // Update persistent messages. The following methods should only be called if we're on the + // lock screen: + updateLockScreenDisclosureMsg(); + updateLockScreenOwnerInfo(); + updateLockScreenBatteryMsg(animate); + updateLockScreenUserLockedMsg(userId); + updateLockScreenTrustMsg(userId, getTrustGrantedIndication(), getTrustManagedIndication()); + updateLockScreenAlignmentMsg(); + updateLockScreenLogoutView(); + updateLockScreenRestingMsg(); } private void updateOrganizedOwnedDevice() { // avoid calling this method since it has an IPC mOrganizationOwnedDevice = whitelistIpcs(this::isOrganizationOwnedDevice); - updatePersistentIndications(false, KeyguardUpdateMonitor.getCurrentUser()); + updateDeviceEntryIndication(false); } - private void updateDisclosure() { + private void updateLockScreenDisclosureMsg() { if (mOrganizationOwnedDevice) { mBackgroundExecutor.execute(() -> { final CharSequence organizationName = getOrganizationOwnedDeviceOrganizationName(); @@ -374,7 +392,7 @@ public class KeyguardIndicationController { } } - private void updateOwnerInfo() { + private void updateLockScreenOwnerInfo() { // Check device owner info on a bg thread. // It makes multiple IPCs that could block the thread it's run on. mBackgroundExecutor.execute(() -> { @@ -406,7 +424,7 @@ public class KeyguardIndicationController { }); } - private void updateBattery(boolean animate) { + private void updateLockScreenBatteryMsg(boolean animate) { if (mPowerPluggedIn || mEnableBatteryDefender) { String powerIndication = computePowerIndication(); if (DEBUG_CHARGING_SPEED) { @@ -426,7 +444,7 @@ public class KeyguardIndicationController { } } - private void updateUserLocked(int userId) { + private void updateLockScreenUserLockedMsg(int userId) { if (!mKeyguardUpdateMonitor.isUserUnlocked(userId)) { mRotateTextViewController.updateIndication( INDICATION_TYPE_USER_LOCKED, @@ -442,6 +460,11 @@ public class KeyguardIndicationController { } private void updateBiometricMessage() { + if (mDozing) { + updateDeviceEntryIndication(false); + return; + } + if (!TextUtils.isEmpty(mBiometricMessage)) { mRotateTextViewController.updateIndication( INDICATION_TYPE_BIOMETRIC_MESSAGE, @@ -455,25 +478,22 @@ public class KeyguardIndicationController { } else { mRotateTextViewController.hideIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE); } + } + private void updateTransient() { if (mDozing) { - updateIndication(false); + updateDeviceEntryIndication(false); + return; } - } - private void updateTransient() { if (!TextUtils.isEmpty(mTransientIndication)) { mRotateTextViewController.showTransient(mTransientIndication); } else { mRotateTextViewController.hideTransient(); } - - if (mDozing) { - updateIndication(false); - } } - private void updateTrust(int userId, CharSequence trustGrantedIndication, + private void updateLockScreenTrustMsg(int userId, CharSequence trustGrantedIndication, CharSequence trustManagedIndication) { if (!TextUtils.isEmpty(trustGrantedIndication) && mKeyguardUpdateMonitor.getUserHasTrust(userId)) { @@ -499,7 +519,7 @@ public class KeyguardIndicationController { } } - private void updateAlignment() { + private void updateLockScreenAlignmentMsg() { if (!TextUtils.isEmpty(mAlignmentIndication)) { mRotateTextViewController.updateIndication( INDICATION_TYPE_ALIGNMENT, @@ -514,7 +534,7 @@ public class KeyguardIndicationController { } } - private void updateResting() { + private void updateLockScreenRestingMsg() { if (!TextUtils.isEmpty(mRestingIndication) && !mRotateTextViewController.hasIndications()) { mRotateTextViewController.updateIndication( @@ -529,7 +549,7 @@ public class KeyguardIndicationController { } } - private void updateLogoutView() { + private void updateLockScreenLogoutView() { final boolean shouldShowLogout = mKeyguardUpdateMonitor.isLogoutEnabled() && KeyguardUpdateMonitor.getCurrentUser() != UserHandle.USER_SYSTEM; if (shouldShowLogout) { @@ -608,7 +628,7 @@ public class KeyguardIndicationController { if (!mHandler.hasMessages(MSG_HIDE_TRANSIENT)) { hideTransientIndication(); } - updateIndication(false); + updateDeviceEntryIndication(false); } else if (!visible) { // If we unlock and return to keyguard quickly, previous error should not be shown hideTransientIndication(); @@ -620,7 +640,7 @@ public class KeyguardIndicationController { */ public void setRestingIndication(String restingIndication) { mRestingIndication = restingIndication; - updateIndication(false); + updateDeviceEntryIndication(false); } /** @@ -697,6 +717,10 @@ public class KeyguardIndicationController { * Shows {@param biometricMessage} until it is hidden by {@link #hideBiometricMessage}. */ private void showBiometricMessage(CharSequence biometricMessage) { + if (TextUtils.equals(biometricMessage, mBiometricMessage)) { + return; + } + mBiometricMessage = biometricMessage; mHandler.removeMessages(MSG_SHOW_ACTION_TO_UNLOCK); @@ -725,7 +749,12 @@ public class KeyguardIndicationController { } } - protected final void updateIndication(boolean animate) { + /** + * Updates message shown to the user. If the device is dozing, a single message with the highest + * precedence is shown. If the device is not dozing (on the lock screen), then several messages + * may continuously be cycled through. + */ + protected final void updateDeviceEntryIndication(boolean animate) { if (!mVisible) { return; } @@ -734,44 +763,37 @@ public class KeyguardIndicationController { mIndicationArea.setVisibility(VISIBLE); // Walk down a precedence-ordered list of what indication - // should be shown based on user or device state - // AoD + // should be shown based on device state if (mDozing) { mLockScreenIndicationView.setVisibility(View.GONE); mTopIndicationView.setVisibility(VISIBLE); // When dozing we ignore any text color and use white instead, because // colors can be hard to read in low brightness. mTopIndicationView.setTextColor(Color.WHITE); + + CharSequence newIndication = null; if (!TextUtils.isEmpty(mBiometricMessage)) { - mWakeLock.setAcquired(true); - mTopIndicationView.switchIndication(mBiometricMessage, null, - true, () -> mWakeLock.setAcquired(false)); + newIndication = mBiometricMessage; } else if (!TextUtils.isEmpty(mTransientIndication)) { - mWakeLock.setAcquired(true); - mTopIndicationView.switchIndication(mTransientIndication, null, - true, () -> mWakeLock.setAcquired(false)); + newIndication = mTransientIndication; } else if (!mBatteryPresent) { // If there is no battery detected, hide the indication and bail mIndicationArea.setVisibility(GONE); + return; } else if (!TextUtils.isEmpty(mAlignmentIndication)) { - mTopIndicationView.switchIndication(mAlignmentIndication, null, - false /* animate */, null /* onAnimationEndCallback */); + newIndication = mAlignmentIndication; mTopIndicationView.setTextColor(mContext.getColor(R.color.misalignment_text_color)); } else if (mPowerPluggedIn || mEnableBatteryDefender) { - String indication = computePowerIndication(); - if (animate) { - mWakeLock.setAcquired(true); - mTopIndicationView.switchIndication(indication, null, true /* animate */, - () -> mWakeLock.setAcquired(false)); - } else { - mTopIndicationView.switchIndication(indication, null, false /* animate */, - null /* onAnimationEndCallback */); - } + newIndication = computePowerIndication(); } else { - String percentage = NumberFormat.getPercentInstance() + newIndication = NumberFormat.getPercentInstance() .format(mBatteryLevel / 100f); - mTopIndicationView.switchIndication(percentage, null /* indication */, - false /* animate */, null /* onAnimationEnd*/); + } + + if (!TextUtils.equals(mTopIndicationView.getText(), newIndication)) { + mWakeLock.setAcquired(true); + mTopIndicationView.switchIndication(newIndication, null, + true, () -> mWakeLock.setAcquired(false)); } return; } @@ -780,7 +802,7 @@ public class KeyguardIndicationController { mTopIndicationView.setVisibility(GONE); mTopIndicationView.setText(null); mLockScreenIndicationView.setVisibility(View.VISIBLE); - updatePersistentIndications(animate, KeyguardUpdateMonitor.getCurrentUser()); + updateLockScreenIndications(animate, KeyguardUpdateMonitor.getCurrentUser()); } protected String computePowerIndication() { @@ -842,29 +864,6 @@ public class KeyguardIndicationController { mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; } - private final KeyguardUpdateMonitorCallback mTickReceiver = - new KeyguardUpdateMonitorCallback() { - @Override - public void onTimeChanged() { - if (mVisible) { - updateIndication(false /* animate */); - } - } - }; - - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - if (msg.what == MSG_HIDE_TRANSIENT) { - hideTransientIndication(); - } else if (msg.what == MSG_SHOW_ACTION_TO_UNLOCK) { - showActionToUnlock(); - } else if (msg.what == MSG_HIDE_BIOMETRIC_MESSAGE) { - hideBiometricMessage(); - } - } - }; - /** * Show message on the keyguard for how the user can unlock/enter their device. */ @@ -929,7 +928,7 @@ public class KeyguardIndicationController { pw.println(" mBiometricMessage: " + mBiometricMessage); pw.println(" mBatteryLevel: " + mBatteryLevel); pw.println(" mBatteryPresent: " + mBatteryPresent); - pw.println(" mTextView.getText(): " + ( + pw.println(" AOD text: " + ( mTopIndicationView == null ? null : mTopIndicationView.getText())); pw.println(" computePowerIndication(): " + computePowerIndication()); pw.println(" trustGrantedIndication: " + getTrustGrantedIndication()); @@ -940,6 +939,13 @@ public class KeyguardIndicationController { public static final int HIDE_DELAY_MS = 5000; @Override + public void onTimeChanged() { + if (mVisible) { + updateDeviceEntryIndication(false /* animate */); + } + } + + @Override public void onRefreshBatteryInfo(BatteryStatus status) { boolean isChargingOrFull = status.status == BatteryManager.BATTERY_STATUS_CHARGING || status.isCharged(); @@ -962,7 +968,7 @@ public class KeyguardIndicationController { Log.e(TAG, "Error calling IBatteryStats: ", e); mChargingTimeRemaining = -1; } - updateIndication(!wasPluggedIn && mPowerPluggedInWired); + updateDeviceEntryIndication(!wasPluggedIn && mPowerPluggedInWired); if (mDozing) { if (!wasPluggedIn && mPowerPluggedIn) { showTransientIndication(computePowerIndication()); @@ -1084,14 +1090,13 @@ public class KeyguardIndicationController { if (KeyguardUpdateMonitor.getCurrentUser() != userId) { return; } - updateTrust(userId, getTrustGrantedIndication(), getTrustManagedIndication()); + updateDeviceEntryIndication(false); } @Override public void showTrustGrantedMessage(CharSequence message) { mTrustGrantedIndication = message; - updateTrust(KeyguardUpdateMonitor.getCurrentUser(), getTrustGrantedIndication(), - getTrustManagedIndication()); + updateDeviceEntryIndication(false); } @Override @@ -1125,21 +1130,21 @@ public class KeyguardIndicationController { @Override public void onUserSwitchComplete(int userId) { if (mVisible) { - updateIndication(false); + updateDeviceEntryIndication(false); } } @Override public void onUserUnlocked() { if (mVisible) { - updateIndication(false); + updateDeviceEntryIndication(false); } } @Override public void onLogoutEnabledChanged() { if (mVisible) { - updateIndication(false); + updateDeviceEntryIndication(false); } } @@ -1167,7 +1172,7 @@ public class KeyguardIndicationController { if (mDozing) { hideBiometricMessage(); } - updateIndication(false); + updateDeviceEntryIndication(false); } }; @@ -1175,7 +1180,7 @@ public class KeyguardIndicationController { new KeyguardStateController.Callback() { @Override public void onUnlockedChanged() { - updateIndication(false); + updateDeviceEntryIndication(false); } @Override @@ -1185,7 +1190,7 @@ public class KeyguardIndicationController { mTopIndicationView.clearMessages(); mRotateTextViewController.clearMessages(); } else { - updatePersistentIndications(false, KeyguardUpdateMonitor.getCurrentUser()); + updateDeviceEntryIndication(false); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index ebe91c7dc5d0..8b25c2bc20b9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -292,20 +292,11 @@ public class DozeParameters implements } public void updateControlScreenOff() { - final boolean controlScreenOff = shouldControlUnlockedScreenOff() - || (!getDisplayNeedsBlanking() && getAlwaysOn() && mKeyguardShowing); - setControlScreenOffAnimation(controlScreenOff); - } - - /** - * Whether we're capable of controlling the screen off animation if we want to. This isn't - * possible if AOD isn't even enabled or if the flag is disabled, or if the display needs - * blanking. - */ - public boolean canControlUnlockedScreenOff() { - return getAlwaysOn() - && mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) - && !getDisplayNeedsBlanking(); + if (!getDisplayNeedsBlanking()) { + final boolean controlScreenOff = + getAlwaysOn() && (mKeyguardShowing || shouldControlUnlockedScreenOff()); + setControlScreenOffAnimation(controlScreenOff); + } } /** @@ -318,7 +309,8 @@ public class DozeParameters implements * disabled for a11y. */ public boolean shouldControlUnlockedScreenOff() { - return mUnlockedScreenOffAnimationController.shouldPlayUnlockedScreenOffAnimation(); + return canControlUnlockedScreenOff() + && mUnlockedScreenOffAnimationController.shouldPlayUnlockedScreenOffAnimation(); } public boolean shouldDelayKeyguardShow() { @@ -350,6 +342,16 @@ public class DozeParameters implements return getAlwaysOn() && mKeyguardShowing; } + /** + * Whether we're capable of controlling the screen off animation if we want to. This isn't + * possible if AOD isn't even enabled or if the flag is disabled. + */ + public boolean canControlUnlockedScreenOff() { + return getAlwaysOn() + && mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) + && !getDisplayNeedsBlanking(); + } + private boolean getBoolean(String propName, int resId) { return SystemProperties.getBoolean(propName, mResources.getBoolean(resId)); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 8b0eaec5dcf4..c11d450e47b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -61,14 +61,6 @@ class UnlockedScreenOffAnimationController @Inject constructor( ) : WakefulnessLifecycle.Observer, ScreenOffAnimation { private lateinit var mCentralSurfaces: CentralSurfaces - - /** - * Whether or not [initialize] has been called to provide us with the StatusBar, - * NotificationPanelViewController, and LightRevealSrim so that we can run the unlocked screen - * off animation. - */ - private var initialized = false - private lateinit var lightRevealScrim: LightRevealScrim private var animatorDurationScale = 1f @@ -124,7 +116,6 @@ class UnlockedScreenOffAnimationController @Inject constructor( centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim ) { - this.initialized = true this.lightRevealScrim = lightRevealScrim this.mCentralSurfaces = centralSurfaces @@ -271,18 +262,6 @@ class UnlockedScreenOffAnimationController @Inject constructor( * on the current state of the device. */ fun shouldPlayUnlockedScreenOffAnimation(): Boolean { - // If we haven't been initialized yet, we don't have a StatusBar/LightRevealScrim yet, so we - // can't perform the animation. - if (!initialized) { - return false - } - - // If the device isn't in a state where we can control unlocked screen off (no AOD enabled, - // power save, etc.) then we shouldn't try to do so. - if (!dozeParameters.get().canControlUnlockedScreenOff()) { - return false - } - // If we explicitly already decided not to play the screen off animation, then never change // our mind. if (decidedToAnimateGoingToSleep == false) { @@ -325,7 +304,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( } override fun shouldDelayDisplayDozeTransition(): Boolean = - shouldPlayUnlockedScreenOffAnimation() + dozeParameters.get().shouldControlUnlockedScreenOff() /** * Whether we're doing the light reveal animation or we're done with that and animating in the diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java index 796af115bf68..58b4af43a9b7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IWindowMagnificationConnectionTest.java @@ -134,6 +134,16 @@ public class IWindowMagnificationConnectionTest extends SysuiTestCase { } @Test + public void moveWindowMagnifierToPosition() throws RemoteException { + mIWindowMagnificationConnection.moveWindowMagnifierToPosition(TEST_DISPLAY, + 100f, 200f, mAnimationCallback); + waitForIdleSync(); + + verify(mWindowMagnificationController).moveWindowMagnifierToPosition( + eq(100f), eq(200f), any(IRemoteMagnificationAnimationCallback.class)); + } + + @Test public void showMagnificationButton() throws RemoteException { mIWindowMagnificationConnection.showMagnificationButton(TEST_DISPLAY, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MockMagnificationAnimationCallback.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MockMagnificationAnimationCallback.java new file mode 100644 index 000000000000..30bff0943da7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MockMagnificationAnimationCallback.java @@ -0,0 +1,54 @@ +/* + * 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.accessibility; + +import android.os.RemoteException; +import android.view.accessibility.IRemoteMagnificationAnimationCallback; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +public class MockMagnificationAnimationCallback extends IRemoteMagnificationAnimationCallback.Stub { + + private final CountDownLatch mCountDownLatch; + private final AtomicInteger mSuccessCount; + private final AtomicInteger mFailedCount; + + MockMagnificationAnimationCallback(CountDownLatch countDownLatch) { + mCountDownLatch = countDownLatch; + mSuccessCount = new AtomicInteger(); + mFailedCount = new AtomicInteger(); + } + + public int getSuccessCount() { + return mSuccessCount.get(); + } + + public int getFailedCount() { + return mFailedCount.get(); + } + + @Override + public void onResult(boolean success) throws RemoteException { + mCountDownLatch.countDown(); + if (success) { + mSuccessCount.getAndIncrement(); + } else { + mFailedCount.getAndIncrement(); + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index 3cc177dd8d91..21c3d6ea0660 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -60,6 +60,8 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @Ignore @@ -218,6 +220,29 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { } @Test + public void enableWindowMagnificationWithUnchanged_enabling_expectedValuesToDefault() + throws InterruptedException { + final CountDownLatch countDownLatch = new CountDownLatch(2); + final MockMagnificationAnimationCallback animationCallback = + new MockMagnificationAnimationCallback(countDownLatch); + + enableWindowMagnificationAndWaitAnimating(mWaitIntermediateAnimationPeriod, + animationCallback); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationAnimationController.enableWindowMagnification(Float.NaN, + Float.NaN, Float.NaN, animationCallback); + }); + + assertTrue(countDownLatch.await(mWaitingAnimationPeriod, TimeUnit.MILLISECONDS)); + // The callback in 2nd enableWindowMagnification will return true + assertEquals(1, animationCallback.getSuccessCount()); + // The callback in 1st enableWindowMagnification will return false + assertEquals(1, animationCallback.getFailedCount()); + verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y); + } + + @Test public void enableWindowMagnificationWithScaleOne_enabled_AnimationAndInvokeCallback() throws RemoteException { enableWindowMagnificationWithoutAnimation(); @@ -425,6 +450,102 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { } @Test + public void moveWindowMagnifierToPosition_enabled_expectedValues() + throws InterruptedException { + final CountDownLatch countDownLatch = new CountDownLatch(1); + final MockMagnificationAnimationCallback animationCallback = + new MockMagnificationAnimationCallback(countDownLatch); + final float targetCenterX = DEFAULT_CENTER_X + 100; + final float targetCenterY = DEFAULT_CENTER_Y + 100; + enableWindowMagnificationWithoutAnimation(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationAnimationController.moveWindowMagnifierToPosition( + targetCenterX, targetCenterY, animationCallback); + }); + + assertTrue(countDownLatch.await(mWaitingAnimationPeriod, TimeUnit.MILLISECONDS)); + assertEquals(1, animationCallback.getSuccessCount()); + assertEquals(0, animationCallback.getFailedCount()); + verifyFinalSpec(DEFAULT_SCALE, targetCenterX, targetCenterY); + } + + @Test + public void moveWindowMagnifierToPositionMultipleTimes_enabled_expectedValuesToLastOne() + throws InterruptedException { + final CountDownLatch countDownLatch = new CountDownLatch(4); + final MockMagnificationAnimationCallback animationCallback = + new MockMagnificationAnimationCallback(countDownLatch); + enableWindowMagnificationWithoutAnimation(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationAnimationController.moveWindowMagnifierToPosition( + DEFAULT_CENTER_X + 10, DEFAULT_CENTER_Y + 10, animationCallback); + mWindowMagnificationAnimationController.moveWindowMagnifierToPosition( + DEFAULT_CENTER_X + 20, DEFAULT_CENTER_Y + 20, animationCallback); + mWindowMagnificationAnimationController.moveWindowMagnifierToPosition( + DEFAULT_CENTER_X + 30, DEFAULT_CENTER_Y + 30, animationCallback); + mWindowMagnificationAnimationController.moveWindowMagnifierToPosition( + DEFAULT_CENTER_X + 40, DEFAULT_CENTER_Y + 40, animationCallback); + }); + + assertTrue(countDownLatch.await(mWaitingAnimationPeriod, TimeUnit.MILLISECONDS)); + // only the last one callback will return true + assertEquals(1, animationCallback.getSuccessCount()); + // the others will return false + assertEquals(3, animationCallback.getFailedCount()); + verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X + 40, DEFAULT_CENTER_Y + 40); + } + + @Test + public void moveWindowMagnifierToPosition_enabling_expectedValuesToLastOne() + throws InterruptedException { + final CountDownLatch countDownLatch = new CountDownLatch(2); + final MockMagnificationAnimationCallback animationCallback = + new MockMagnificationAnimationCallback(countDownLatch); + final float targetCenterX = DEFAULT_CENTER_X + 100; + final float targetCenterY = DEFAULT_CENTER_Y + 100; + + enableWindowMagnificationAndWaitAnimating(mWaitIntermediateAnimationPeriod, + animationCallback); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationAnimationController.moveWindowMagnifierToPosition( + targetCenterX, targetCenterY, animationCallback); + }); + + assertTrue(countDownLatch.await(mWaitingAnimationPeriod, TimeUnit.MILLISECONDS)); + // The callback in moveWindowMagnifierToPosition will return true + assertEquals(1, animationCallback.getSuccessCount()); + // The callback in enableWindowMagnification will return false + assertEquals(1, animationCallback.getFailedCount()); + verifyFinalSpec(DEFAULT_SCALE, targetCenterX, targetCenterY); + } + + @Test + public void moveWindowMagnifierToPositionWithCenterUnchanged_enabling_expectedValuesToDefault() + throws InterruptedException { + final CountDownLatch countDownLatch = new CountDownLatch(2); + final MockMagnificationAnimationCallback animationCallback = + new MockMagnificationAnimationCallback(countDownLatch); + + enableWindowMagnificationAndWaitAnimating(mWaitIntermediateAnimationPeriod, + animationCallback); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationAnimationController.moveWindowMagnifierToPosition( + Float.NaN, Float.NaN, animationCallback); + }); + + assertTrue(countDownLatch.await(mWaitingAnimationPeriod, TimeUnit.MILLISECONDS)); + // The callback in moveWindowMagnifierToPosition will return true + assertEquals(1, animationCallback.getSuccessCount()); + // The callback in enableWindowMagnification will return false + assertEquals(1, animationCallback.getFailedCount()); + verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y); + } + + @Test public void enableWindowMagnificationWithSameScale_enabled_doNothingButInvokeCallback() throws RemoteException { enableWindowMagnificationAndWaitAnimating(mWaitingAnimationPeriod, null); @@ -569,6 +690,20 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X + 100f, DEFAULT_CENTER_Y + 100f); } + @Test + public void moveWindowMagnifierToPosition_enabled() { + final float targetCenterX = DEFAULT_CENTER_X + 100; + final float targetCenterY = DEFAULT_CENTER_Y + 100; + enableWindowMagnificationWithoutAnimation(); + + mInstrumentation.runOnMainSync( + () -> mController.moveWindowMagnifierToPosition(targetCenterX, targetCenterY, + mAnimationCallback)); + SystemClock.sleep(mWaitingAnimationPeriod); + + verifyFinalSpec(DEFAULT_SCALE, targetCenterX, targetCenterY); + } + private void verifyFinalSpec(float expectedScale, float expectedCenterX, float expectedCenterY) { assertEquals(expectedScale, mController.getScale(), 0f); @@ -663,6 +798,13 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { } @Override + void moveWindowMagnifierToPosition(float positionX, float positionY, + IRemoteMagnificationAnimationCallback callback) { + super.moveWindowMagnifierToPosition(positionX, positionY, callback); + mSpyController.moveWindowMagnifierToPosition(positionX, positionY, callback); + } + + @Override void setScale(float scale) { super.setScale(scale); mSpyController.setScale(scale); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index 6e5926db519d..a49c4d76445b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -40,6 +40,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -88,6 +89,8 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @LargeTest @@ -96,12 +99,16 @@ import java.util.concurrent.atomic.AtomicInteger; public class WindowMagnificationControllerTest extends SysuiTestCase { private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000; + private static final long ANIMATION_DURATION_MS = 300; + private final long mWaitingAnimationPeriod = 2 * ANIMATION_DURATION_MS; @Mock private SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; @Mock private MirrorWindowControl mMirrorWindowControl; @Mock private WindowMagnifierCallback mWindowMagnifierCallback; + @Mock + IRemoteMagnificationAnimationCallback mAnimationCallback; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); @@ -287,6 +294,82 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { } @Test + public void moveWindowMagnifierToPositionWithAnimation_expectedValuesAndInvokeCallback() + throws InterruptedException { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, + Float.NaN, 0, 0, null); + }); + final CountDownLatch countDownLatch = new CountDownLatch(1); + final MockMagnificationAnimationCallback animationCallback = + new MockMagnificationAnimationCallback(countDownLatch); + final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + final float targetCenterX = sourceBoundsCaptor.getValue().exactCenterX() + 10; + final float targetCenterY = sourceBoundsCaptor.getValue().exactCenterY() + 10; + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifierToPosition( + targetCenterX, targetCenterY, animationCallback); + }); + + assertTrue(countDownLatch.await(mWaitingAnimationPeriod, TimeUnit.MILLISECONDS)); + assertEquals(1, animationCallback.getSuccessCount()); + assertEquals(0, animationCallback.getFailedCount()); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + assertEquals(mWindowMagnificationController.getCenterX(), + sourceBoundsCaptor.getValue().exactCenterX(), 0); + assertEquals(mWindowMagnificationController.getCenterY(), + sourceBoundsCaptor.getValue().exactCenterY(), 0); + assertEquals(mWindowMagnificationController.getCenterX(), targetCenterX, 0); + assertEquals(mWindowMagnificationController.getCenterY(), targetCenterY, 0); + } + + @Test + public void moveWindowMagnifierToPositionMultipleTimes_expectedValuesAndInvokeCallback() + throws InterruptedException { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, + Float.NaN, 0, 0, null); + }); + final CountDownLatch countDownLatch = new CountDownLatch(4); + final MockMagnificationAnimationCallback animationCallback = + new MockMagnificationAnimationCallback(countDownLatch); + final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + final float centerX = sourceBoundsCaptor.getValue().exactCenterX(); + final float centerY = sourceBoundsCaptor.getValue().exactCenterY(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 10, centerY + 10, animationCallback); + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 20, centerY + 20, animationCallback); + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 30, centerY + 30, animationCallback); + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 40, centerY + 40, animationCallback); + }); + + assertTrue(countDownLatch.await(mWaitingAnimationPeriod, TimeUnit.MILLISECONDS)); + // only the last one callback will return true + assertEquals(1, animationCallback.getSuccessCount()); + // the others will return false + assertEquals(3, animationCallback.getFailedCount()); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + assertEquals(mWindowMagnificationController.getCenterX(), + sourceBoundsCaptor.getValue().exactCenterX(), 0); + assertEquals(mWindowMagnificationController.getCenterY(), + sourceBoundsCaptor.getValue().exactCenterY(), 0); + assertEquals(mWindowMagnificationController.getCenterX(), centerX + 40, 0); + assertEquals(mWindowMagnificationController.getCenterY(), centerY + 40, 0); + } + + @Test public void setScale_enabled_expectedValueAndUpdateStateDescription() { mInstrumentation.runOnMainSync( () -> mWindowMagnificationController.enableWindowMagnificationInternal(2.0f, @@ -484,6 +567,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { mirrorView.performAccessibilityAction(R.id.accessibility_action_move_right, null)); assertTrue( mirrorView.performAccessibilityAction(R.id.accessibility_action_move_left, null)); + verify(mWindowMagnifierCallback, times(4)).onMove(eq(displayId)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java index d3f30c508b8b..ccf2f8b16f8a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationTest.java @@ -148,13 +148,13 @@ public class WindowMagnificationTest extends SysuiTestCase { } @Test - public void onDrag_enabled_notifyCallback() throws RemoteException { + public void onMove_enabled_notifyCallback() throws RemoteException { mCommandQueue.requestWindowMagnificationConnection(true); waitForIdleSync(); - mWindowMagnification.onDrag(TEST_DISPLAY); + mWindowMagnification.onMove(TEST_DISPLAY); - verify(mConnectionCallback).onDrag(TEST_DISPLAY); + verify(mConnectionCallback).onMove(TEST_DISPLAY); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java deleted file mode 100644 index 619d48d1e306..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceToFingerprintViewTest.java +++ /dev/null @@ -1,344 +0,0 @@ -/* - * Copyright (C) 2021 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 static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; -import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; - -import android.content.Context; -import android.hardware.biometrics.ComponentInfoInternal; -import android.hardware.biometrics.SensorLocationInternal; -import android.hardware.biometrics.SensorProperties; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; -import android.os.Bundle; -import android.test.suitebuilder.annotation.SmallTest; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import com.android.systemui.R; -import com.android.systemui.SysuiTestCase; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.List; - -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -@SmallTest -public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase { - - @Mock AuthBiometricView.Callback mCallback; - - private AuthBiometricFaceToFingerprintView mFaceToFpView; - - @Mock private Button mNegativeButton; - @Mock private Button mCancelButton; - @Mock private Button mConfirmButton; - @Mock private Button mUseCredentialButton; - @Mock private Button mTryAgainButton; - - @Mock private TextView mTitleView; - @Mock private TextView mSubtitleView; - @Mock private TextView mDescriptionView; - @Mock private TextView mIndicatorView; - @Mock private ImageView mIconView; - @Mock private View mIconHolderView; - @Mock private AuthBiometricFaceView.IconController mFaceIconController; - @Mock private AuthBiometricFaceToFingerprintView.UdfpsIconController mUdfpsIconController; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - - mFaceToFpView = new TestableView(mContext); - mFaceToFpView.mFaceIconController = mFaceIconController; - mFaceToFpView.mUdfpsIconController = mUdfpsIconController; - mFaceToFpView.setCallback(mCallback); - - mFaceToFpView.mNegativeButton = mNegativeButton; - mFaceToFpView.mCancelButton = mCancelButton; - mFaceToFpView.mUseCredentialButton = mUseCredentialButton; - mFaceToFpView.mConfirmButton = mConfirmButton; - mFaceToFpView.mTryAgainButton = mTryAgainButton; - mFaceToFpView.mIndicatorView = mIndicatorView; - } - - @Test - public void testStateUpdated_whenDialogAnimatedIn() { - mFaceToFpView.onDialogAnimatedIn(); - verify(mFaceToFpView.mFaceIconController) - .updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING)); - verify(mFaceToFpView.mUdfpsIconController, never()).updateState(anyInt()); - } - - @Test - public void testIconUpdatesState_whenDialogStateUpdated() { - mFaceToFpView.onDialogAnimatedIn(); - verify(mFaceToFpView.mFaceIconController) - .updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING)); - verify(mFaceToFpView.mUdfpsIconController, never()).updateState(anyInt()); - - mFaceToFpView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATED); - verify(mFaceToFpView.mFaceIconController).updateState( - eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING), - eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATED)); - verify(mFaceToFpView.mUdfpsIconController, never()).updateState(anyInt()); - - assertEquals(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATED, mFaceToFpView.mState); - } - - @Test - public void testStateUpdated_whenSwitchToFingerprint() { - mFaceToFpView.onDialogAnimatedIn(); - verify(mFaceToFpView.mFaceIconController) - .updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING)); - - mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_ERROR); - - verify(mFaceToFpView.mFaceIconController).deactivate(); - verify(mFaceToFpView.mUdfpsIconController).updateState( - eq(AuthBiometricFaceToFingerprintView.STATE_IDLE)); - verify(mConfirmButton).setVisibility(eq(View.GONE)); - - mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING); - - verify(mFaceToFpView.mUdfpsIconController).updateState( - eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING)); - } - - @Test - public void testStateUpdated_whenSwitchToFingerprint_invokesCallbacks() { - class TestModalityListener implements ModalityListener { - public int switchCount = 0; - - @Override - public void onModalitySwitched(int oldModality, int newModality) { - assertEquals(TYPE_FINGERPRINT, newModality); - assertEquals(TYPE_FACE, oldModality); - switchCount++; - } - } - final TestModalityListener modalityListener = new TestModalityListener(); - - mFaceToFpView.onDialogAnimatedIn(); - mFaceToFpView.setModalityListener(modalityListener); - - assertEquals(0, modalityListener.switchCount); - - mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_ERROR); - mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING); - - assertEquals(1, modalityListener.switchCount); - } - - @Test - @Ignore("flaky, b/189031816") - public void testModeUpdated_onSoftError_whenSwitchToFingerprint() { - mFaceToFpView.onDialogAnimatedIn(); - mFaceToFpView.onAuthenticationFailed(TYPE_FACE, "no face"); - waitForIdleSync(); - - verify(mIndicatorView).setText( - eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead))); - verify(mCallback).onAction( - eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR)); - - // First we enter the error state, since we need to show the error animation/text. The - // error state is later cleared based on a timer, and we enter STATE_AUTHENTICATING. - assertEquals(AuthBiometricFaceToFingerprintView.STATE_ERROR, mFaceToFpView.mState); - } - - @Test - @Ignore("flaky, b/189031816") - public void testModeUpdated_onHardError_whenSwitchToFingerprint() { - mFaceToFpView.onDialogAnimatedIn(); - mFaceToFpView.onError(TYPE_FACE, "oh no!"); - waitForIdleSync(); - - verify(mIndicatorView).setText( - eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead))); - verify(mCallback).onAction( - eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR)); - - // First we enter the error state, since we need to show the error animation/text. The - // error state is later cleared based on a timer, and we enter STATE_AUTHENTICATING. - assertEquals(AuthBiometricFaceToFingerprintView.STATE_ERROR, mFaceToFpView.mState); - } - - @Test - public void testFingerprintOnlyStartsOnFirstError() { - mFaceToFpView.onDialogAnimatedIn(); - verify(mFaceToFpView.mFaceIconController) - .updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING)); - - mFaceToFpView.onDialogAnimatedIn(); - mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_ERROR); - mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING); - - reset(mCallback); - - mFaceToFpView.onError(TYPE_FACE, "oh no!"); - mFaceToFpView.onAuthenticationFailed(TYPE_FACE, "no face"); - - verify(mCallback, never()).onAction( - eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR)); - } - - @Test - public void testOnSaveState() { - final FingerprintSensorPropertiesInternal sensorProps = createFingerprintSensorProps(); - mFaceToFpView.setFingerprintSensorProps(sensorProps); - - final Bundle savedState = new Bundle(); - mFaceToFpView.onSaveState(savedState); - - assertEquals(savedState.getInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE), - mFaceToFpView.getActiveSensorType()); - assertEquals(savedState.getParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS), sensorProps); - } - - @Test - public void testRestoreState() { - final Bundle savedState = new Bundle(); - savedState.putInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, TYPE_FINGERPRINT); - savedState.putParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS, - createFingerprintSensorProps()); - - mFaceToFpView.restoreState(savedState); - - assertEquals(mFaceToFpView.getActiveSensorType(), TYPE_FINGERPRINT); - assertTrue(mFaceToFpView.isFingerprintUdfps()); - } - - @NonNull - private static FingerprintSensorPropertiesInternal createFingerprintSensorProps() { - final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); - componentInfo.add(new ComponentInfoInternal("componentId", "hardwareVersion", - "firmwareVersion", "serialNumber", "softwareVersion")); - - return new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, - SensorProperties.STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - true /* resetLockoutRequiresHardwareAuthToken */, - List.of(new SensorLocationInternal("" /* displayId */, - 540 /* sensorLocationX */, - 1600 /* sensorLocationY */, - 100 /* sensorRadius */))); - } - - public class TestableView extends AuthBiometricFaceToFingerprintView { - public TestableView(Context context) { - super(context, null, new MockInjector()); - } - - @Override - protected int getDelayAfterAuthenticatedDurationMs() { - return 0; - } - } - - private class MockInjector extends AuthBiometricView.Injector { - @Override - public Button getNegativeButton() { - return mNegativeButton; - } - - @Override - public Button getCancelButton() { - return mCancelButton; - } - - @Override - public Button getUseCredentialButton() { - return mUseCredentialButton; - } - - @Override - public Button getConfirmButton() { - return mConfirmButton; - } - - @Override - public Button getTryAgainButton() { - return mTryAgainButton; - } - - @Override - public TextView getTitleView() { - return mTitleView; - } - - @Override - public TextView getSubtitleView() { - return mSubtitleView; - } - - @Override - public TextView getDescriptionView() { - return mDescriptionView; - } - - @Override - public TextView getIndicatorView() { - return mIndicatorView; - } - - @Override - public ImageView getIconView() { - return mIconView; - } - - @Override - public View getIconHolderView() { - return mIconHolderView; - } - - @Override - public int getDelayAfterError() { - return 0; - } - - @Override - public int getMediumToLargeAnimationDurationMs() { - return 0; - } - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java deleted file mode 100644 index b93381d2b5c9..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFaceViewTest.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import android.content.Context; -import android.test.suitebuilder.annotation.SmallTest; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper.RunWithLooper; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.systemui.SysuiTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import com.android.systemui.R; - -@RunWith(AndroidTestingRunner.class) -@RunWithLooper -@SmallTest -public class AuthBiometricFaceViewTest extends SysuiTestCase { - - @Mock - AuthBiometricView.Callback mCallback; - - private TestableFaceView mFaceView; - - @Mock private Button mNegativeButton; - @Mock private Button mCancelButton; - @Mock private Button mUseCredentialButton; - - @Mock private Button mConfirmButton; - @Mock private Button mTryAgainButton; - - @Mock private TextView mErrorView; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mFaceView = new TestableFaceView(mContext); - mFaceView.mFaceIconController = mock(TestableFaceView.TestableIconController.class); - mFaceView.setCallback(mCallback); - - mFaceView.mNegativeButton = mNegativeButton; - mFaceView.mCancelButton = mCancelButton; - mFaceView.mUseCredentialButton = mUseCredentialButton; - - mFaceView.mConfirmButton = mConfirmButton; - mFaceView.mTryAgainButton = mTryAgainButton; - - mFaceView.mIndicatorView = mErrorView; - } - - @Test - public void testStateUpdated_whenDialogAnimatedIn() { - mFaceView.onDialogAnimatedIn(); - verify(mFaceView.mFaceIconController) - .updateState(anyInt(), eq(AuthBiometricFaceView.STATE_AUTHENTICATING)); - } - - @Test - public void testIconUpdatesState_whenDialogStateUpdated() { - mFaceView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATING); - verify(mFaceView.mFaceIconController) - .updateState(anyInt(), eq(AuthBiometricFaceView.STATE_AUTHENTICATING)); - - mFaceView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATED); - verify(mFaceView.mFaceIconController).updateState( - eq(AuthBiometricFaceView.STATE_AUTHENTICATING), - eq(AuthBiometricFaceView.STATE_AUTHENTICATED)); - } - - public class TestableFaceView extends AuthBiometricFaceView { - - public class TestableIconController extends IconController { - TestableIconController(Context context, ImageView iconView) { - super(context, iconView, mock(TextView.class)); - } - - public void startPulsing() { - // Stub for testing - } - } - - @Override - protected int getDelayAfterAuthenticatedDurationMs() { - return 0; // Keep this at 0 for tests to invoke callback immediately. - } - - public TestableFaceView(Context context) { - super(context); - } - } - -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java index f8e38e4994bc..9418b50ff390 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java @@ -20,137 +20,109 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.Authenticators; +import static com.android.systemui.biometrics.AuthBiometricView.Callback.ACTION_AUTHENTICATED; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import android.content.Context; -import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.PromptInfo; import android.os.Bundle; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; -import android.util.AttributeSet; +import android.testing.ViewUtils; +import android.view.LayoutInflater; import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; -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.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +@Ignore @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest public class AuthBiometricViewTest extends SysuiTestCase { - @Mock private AuthBiometricView.Callback mCallback; - @Mock private AuthPanelController mPanelController; - - @Mock private Button mNegativeButton; - @Mock private Button mCancelButton; - @Mock private Button mUseCredentialButton; - - @Mock private Button mPositiveButton; - @Mock private Button mTryAgainButton; - - @Mock private TextView mTitleView; - @Mock private TextView mSubtitleView; - @Mock private TextView mDescriptionView; - @Mock private TextView mIndicatorView; - @Mock private ImageView mIconView; - @Mock private View mIconHolderView; + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); - private TestableBiometricView mBiometricView; + @Mock + private AuthBiometricView.Callback mCallback; + @Mock + private AuthPanelController mPanelController; - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } + private AuthBiometricView mBiometricView; @Test public void testOnAuthenticationSucceeded_noConfirmationRequired_sendsActionAuthenticated() { - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); + initDialog(false /* allowDeviceCredential */, mCallback); // The onAuthenticated runnable is posted when authentication succeeds. - mBiometricView.onAuthenticationSucceeded(); + mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT); waitForIdleSync(); assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState); - verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED); + verify(mCallback).onAction(ACTION_AUTHENTICATED); } @Test public void testOnAuthenticationSucceeded_confirmationRequired_updatesDialogContents() { - final Button negativeButton = new Button(mContext); - final Button cancelButton = new Button(mContext); - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { - @Override - public Button getNegativeButton() { - return negativeButton; - } - - @Override - public Button getCancelButton() { - return cancelButton; - } - }); + initDialog(false /* allowDeviceCredential */, mCallback); mBiometricView.setRequireConfirmation(true); - mBiometricView.onAuthenticationSucceeded(); + mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT); waitForIdleSync(); - assertEquals(AuthBiometricView.STATE_PENDING_CONFIRMATION, mBiometricView.mState); - verify(mCallback, never()).onAction(anyInt()); - assertEquals(View.GONE, negativeButton.getVisibility()); - assertEquals(View.VISIBLE, cancelButton.getVisibility()); - assertTrue(cancelButton.isEnabled()); + // TODO: this should be tested in the subclasses + if (mBiometricView.supportsRequireConfirmation()) { + assertEquals(AuthBiometricView.STATE_PENDING_CONFIRMATION, mBiometricView.mState); + + verify(mCallback, never()).onAction(anyInt()); + + assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility()); + assertEquals(View.VISIBLE, mBiometricView.mCancelButton.getVisibility()); + assertTrue(mBiometricView.mCancelButton.isEnabled()); + + assertTrue(mBiometricView.mConfirmButton.isEnabled()); + assertEquals(mContext.getText(R.string.biometric_dialog_tap_confirm), + mBiometricView.mIndicatorView.getText()); + assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility()); + } else { + assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState); + verify(mCallback).onAction(eq(ACTION_AUTHENTICATED)); + } - verify(mBiometricView.mConfirmButton).setEnabled(eq(true)); - verify(mIndicatorView).setText(eq(R.string.biometric_dialog_tap_confirm)); - verify(mIndicatorView).setVisibility(eq(View.VISIBLE)); } @Test public void testPositiveButton_sendsActionAuthenticated() { - Button button = new Button(mContext); - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { - @Override - public Button getConfirmButton() { - return button; - } - }); - - button.performClick(); + initDialog(false /* allowDeviceCredential */, mCallback); + + mBiometricView.mConfirmButton.performClick(); waitForIdleSync(); - verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_AUTHENTICATED); + verify(mCallback).onAction(ACTION_AUTHENTICATED); assertEquals(AuthBiometricView.STATE_AUTHENTICATED, mBiometricView.mState); } @Test public void testNegativeButton_beforeAuthentication_sendsActionButtonNegative() { - Button button = new Button(mContext); - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { - @Override - public Button getNegativeButton() { - return button; - } - }); + initDialog(false /* allowDeviceCredential */, mCallback); mBiometricView.onDialogAnimatedIn(); - button.performClick(); + mBiometricView.mNegativeButton.performClick(); waitForIdleSync(); verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE); @@ -158,25 +130,14 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testCancelButton_whenPendingConfirmation_sendsActionUserCanceled() { - Button cancelButton = new Button(mContext); - Button negativeButton = new Button(mContext); - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { - @Override - public Button getNegativeButton() { - return negativeButton; - } - @Override - public Button getCancelButton() { - return cancelButton; - } - }); + initDialog(false /* allowDeviceCredential */, mCallback); mBiometricView.setRequireConfirmation(true); - mBiometricView.onAuthenticationSucceeded(); + mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT); - assertEquals(View.GONE, negativeButton.getVisibility()); + assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility()); - cancelButton.performClick(); + mBiometricView.mCancelButton.performClick(); waitForIdleSync(); verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USER_CANCELED); @@ -184,15 +145,9 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testTryAgainButton_sendsActionTryAgain() { - Button button = new Button(mContext); - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { - @Override - public Button getTryAgainButton() { - return button; - } - }); - - button.performClick(); + initDialog(false /* allowDeviceCredential */, mCallback); + + mBiometricView.mTryAgainButton.performClick(); waitForIdleSync(); verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN); @@ -202,7 +157,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test @Ignore("flaky, b/189031816") public void testError_sendsActionError() { - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); + initDialog(false /* allowDeviceCredential */, mCallback); final String testError = "testError"; mBiometricView.onError(TYPE_FACE, testError); waitForIdleSync(); @@ -213,7 +168,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testBackgroundClicked_sendsActionUserCanceled() { - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); + initDialog(false /* allowDeviceCredential */, mCallback); View view = new View(mContext); mBiometricView.setBackgroundView(view); @@ -223,18 +178,18 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testBackgroundClicked_afterAuthenticated_neverSendsUserCanceled() { - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); + initDialog(false /* allowDeviceCredential */, mCallback); View view = new View(mContext); mBiometricView.setBackgroundView(view); - mBiometricView.onAuthenticationSucceeded(); + mBiometricView.onAuthenticationSucceeded(TYPE_FINGERPRINT); view.performClick(); verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_USER_CANCELED)); } @Test public void testBackgroundClicked_whenSmallDialog_neverSendsUserCanceled() { - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); + initDialog(false /* allowDeviceCredential */, mCallback); mBiometricView.mLayoutParams = new AuthDialog.LayoutParams(0, 0); mBiometricView.updateSize(AuthDialog.SIZE_SMALL); @@ -246,7 +201,7 @@ public class AuthBiometricViewTest extends SysuiTestCase { @Test public void testIgnoresUselessHelp() { - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector()); + initDialog(false /* allowDeviceCredential */, mCallback); mBiometricView.onDialogAnimatedIn(); waitForIdleSync(); @@ -256,33 +211,16 @@ public class AuthBiometricViewTest extends SysuiTestCase { mBiometricView.onHelp(TYPE_FINGERPRINT, ""); waitForIdleSync(); - verify(mIndicatorView, never()).setText(any()); + assertEquals("", mBiometricView.mIndicatorView.getText()); verify(mCallback, never()).onAction(eq(AuthBiometricView.Callback.ACTION_ERROR)); assertEquals(AuthBiometricView.STATE_AUTHENTICATING, mBiometricView.mState); } @Test public void testRestoresState() { - final boolean requireConfirmation = true; // set/init from AuthController - - Button tryAgainButton = new Button(mContext); - TextView indicatorView = new TextView(mContext); - initDialog(mContext, false /* allowDeviceCredential */, mCallback, new MockInjector() { - @Override - public Button getTryAgainButton() { - return tryAgainButton; - } - @Override - public TextView getIndicatorView() { - return indicatorView; - } - - @Override - public int getDelayAfterError() { - // keep a real delay to test saving in the error state - return BiometricPrompt.HIDE_DIALOG_DELAY; - } - }); + final boolean requireConfirmation = true; + + initDialog(false /* allowDeviceCredential */, mCallback, null, 10000); final String failureMessage = "testFailureMessage"; mBiometricView.setRequireConfirmation(requireConfirmation); @@ -292,8 +230,8 @@ public class AuthBiometricViewTest extends SysuiTestCase { Bundle state = new Bundle(); mBiometricView.onSaveState(state); - assertEquals(View.VISIBLE, tryAgainButton.getVisibility()); - assertEquals(View.VISIBLE, state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)); + assertEquals(View.GONE, mBiometricView.mTryAgainButton.getVisibility()); + assertEquals(View.GONE, state.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY)); assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState); assertEquals(AuthBiometricView.STATE_ERROR, state.getInt(AuthDialog.KEY_BIOMETRIC_STATE)); @@ -307,25 +245,12 @@ public class AuthBiometricViewTest extends SysuiTestCase { // TODO: Test dialog size. Should move requireConfirmation to buildBiometricPromptBundle // Create new dialog and restore the previous state into it - Button tryAgainButton2 = new Button(mContext); - TextView indicatorView2 = new TextView(mContext); - initDialog(mContext, false /* allowDeviceCredential */, mCallback, state, - new MockInjector() { - @Override - public Button getTryAgainButton() { - return tryAgainButton2; - } - - @Override - public TextView getIndicatorView() { - return indicatorView2; - } - }); + initDialog(false /* allowDeviceCredential */, mCallback, state, 10000); + mBiometricView.mAnimationDurationHideDialog = 10000; mBiometricView.setRequireConfirmation(requireConfirmation); waitForIdleSync(); - // Test restored state - assertEquals(View.VISIBLE, tryAgainButton.getVisibility()); + assertEquals(View.GONE, mBiometricView.mTryAgainButton.getVisibility()); assertEquals(AuthBiometricView.STATE_ERROR, mBiometricView.mState); assertEquals(View.VISIBLE, mBiometricView.mIndicatorView.getVisibility()); @@ -334,23 +259,12 @@ public class AuthBiometricViewTest extends SysuiTestCase { } @Test - public void testCredentialButton_whenDeviceCredentialAllowed() { - final Button negativeButton = new Button(mContext); - final Button useCredentialButton = new Button(mContext); - initDialog(mContext, true /* allowDeviceCredential */, mCallback, new MockInjector() { - @Override - public Button getNegativeButton() { - return negativeButton; - } - - @Override - public Button getUseCredentialButton() { - return useCredentialButton; - } - }); - - assertEquals(View.GONE, negativeButton.getVisibility()); - useCredentialButton.performClick(); + public void testCredentialButton_whenDeviceCredentialAllowed() throws InterruptedException { + initDialog(true /* allowDeviceCredential */, mCallback); + + assertEquals(View.VISIBLE, mBiometricView.mUseCredentialButton.getVisibility()); + assertEquals(View.GONE, mBiometricView.mNegativeButton.getVisibility()); + mBiometricView.mUseCredentialButton.performClick(); waitForIdleSync(); verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL); @@ -369,120 +283,30 @@ public class AuthBiometricViewTest extends SysuiTestCase { return promptInfo; } - private void initDialog(Context context, boolean allowDeviceCredential, - AuthBiometricView.Callback callback, - Bundle savedState, MockInjector injector) { - mBiometricView = new TestableBiometricView(context, null, injector); + private void initDialog(boolean allowDeviceCredential, AuthBiometricView.Callback callback) { + initDialog(allowDeviceCredential, callback, + null /* savedState */, 0 /* hideDelay */); + } + + private void initDialog(boolean allowDeviceCredential, + AuthBiometricView.Callback callback, Bundle savedState, int hideDelay) { + final LayoutInflater inflater = LayoutInflater.from(mContext); + mBiometricView = (AuthBiometricView) inflater.inflate( + R.layout.auth_biometric_view, null, false); + mBiometricView.mAnimationDurationLong = 0; + mBiometricView.mAnimationDurationShort = 0; + mBiometricView.mAnimationDurationHideDialog = hideDelay; mBiometricView.setPromptInfo(buildPromptInfo(allowDeviceCredential)); mBiometricView.setCallback(callback); mBiometricView.restoreState(savedState); - mBiometricView.onFinishInflateInternal(); - mBiometricView.onAttachedToWindowInternal(); - + ViewUtils.attachView(mBiometricView); mBiometricView.setPanelController(mPanelController); + waitForIdleSync(); } - private void initDialog(Context context, boolean allowDeviceCredential, - AuthBiometricView.Callback callback, MockInjector injector) { - initDialog(context, allowDeviceCredential, callback, null /* savedState */, injector); - } - - private class MockInjector extends AuthBiometricView.Injector { - @Override - public Button getNegativeButton() { - return mNegativeButton; - } - - @Override - public Button getCancelButton() { - return mCancelButton; - } - - @Override - public Button getUseCredentialButton() { - return mUseCredentialButton; - } - - @Override - public Button getConfirmButton() { - return mPositiveButton; - } - - @Override - public Button getTryAgainButton() { - return mTryAgainButton; - } - - @Override - public TextView getTitleView() { - return mTitleView; - } - - @Override - public TextView getSubtitleView() { - return mSubtitleView; - } - - @Override - public TextView getDescriptionView() { - return mDescriptionView; - } - - @Override - public TextView getIndicatorView() { - return mIndicatorView; - } - - @Override - public ImageView getIconView() { - return mIconView; - } - - @Override - public View getIconHolderView() { - return mIconHolderView; - } - - @Override - public int getDelayAfterError() { - return 0; // Keep this at 0 for tests to invoke callback immediately. - } - - @Override - public int getMediumToLargeAnimationDurationMs() { - return 0; - } - } - - private class TestableBiometricView extends AuthBiometricView { - TestableBiometricView(Context context, AttributeSet attrs, - Injector injector) { - super(context, attrs, injector); - } - - @Override - protected int getDelayAfterAuthenticatedDurationMs() { - return 0; // Keep this at 0 for tests to invoke callback immediately. - } - - @Override - protected int getStateForAfterError() { - return 0; - } - - @Override - protected void handleResetAfterError() { - - } - - @Override - protected void handleResetAfterHelp() { - - } - - @Override - protected boolean supportsSmallDialog() { - return false; - } + @Override + protected void waitForIdleSync() { + TestableLooper.get(this).processAllMessages(); + super.waitForIdleSync(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java deleted file mode 100644 index ae1268d48af9..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import static android.hardware.biometrics.BiometricManager.Authenticators; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertTrue; - -import static org.junit.Assert.assertNotEquals; -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.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.annotation.Nullable; -import android.content.Context; -import android.hardware.biometrics.BiometricConstants; -import android.hardware.biometrics.ComponentInfoInternal; -import android.hardware.biometrics.PromptInfo; -import android.hardware.biometrics.SensorProperties; -import android.hardware.face.FaceSensorPropertiesInternal; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; -import android.os.IBinder; -import android.os.UserManager; -import android.test.suitebuilder.annotation.SmallTest; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper.RunWithLooper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.ScrollView; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.keyguard.WakefulnessLifecycle; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.List; - -@RunWith(AndroidTestingRunner.class) -@RunWithLooper -@SmallTest -public class AuthContainerViewTest extends SysuiTestCase { - - private TestableAuthContainer mAuthContainer; - - private @Mock AuthDialogCallback mCallback; - private @Mock UserManager mUserManager; - private @Mock WakefulnessLifecycle mWakefulnessLifecycle; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testActionAuthenticated_sendsDismissedAuthenticated() { - initializeContainer(Authenticators.BIOMETRIC_WEAK); - - mAuthContainer.mBiometricCallback.onAction( - AuthBiometricView.Callback.ACTION_AUTHENTICATED); - verify(mCallback).onDismissed( - eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED), - eq(null) /* credentialAttestation */); - } - - @Test - public void testActionUserCanceled_sendsDismissedUserCanceled() { - initializeContainer(Authenticators.BIOMETRIC_WEAK); - - mAuthContainer.mBiometricCallback.onAction( - AuthBiometricView.Callback.ACTION_USER_CANCELED); - verify(mCallback).onSystemEvent(eq( - BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL)); - verify(mCallback).onDismissed( - eq(AuthDialogCallback.DISMISSED_USER_CANCELED), - eq(null) /* credentialAttestation */); - } - - @Test - public void testActionButtonNegative_sendsDismissedButtonNegative() { - initializeContainer(Authenticators.BIOMETRIC_WEAK); - - mAuthContainer.mBiometricCallback.onAction( - AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE); - verify(mCallback).onDismissed( - eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE), - eq(null) /* credentialAttestation */); - } - - @Test - public void testActionTryAgain_sendsTryAgain() { - initializeContainer(Authenticators.BIOMETRIC_WEAK); - - mAuthContainer.mBiometricCallback.onAction( - AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN); - verify(mCallback).onTryAgainPressed(); - } - - @Test - public void testActionError_sendsDismissedError() { - initializeContainer(Authenticators.BIOMETRIC_WEAK); - - mAuthContainer.mBiometricCallback.onAction( - AuthBiometricView.Callback.ACTION_ERROR); - verify(mCallback).onDismissed( - eq(AuthDialogCallback.DISMISSED_ERROR), - eq(null) /* credentialAttestation */); - } - - @Test - public void testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() { - initializeContainer( - Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL); - - mAuthContainer.mBiometricCallback.onAction( - AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL); - verify(mCallback).onDeviceCredentialPressed(); - - // Credential view is attached to the frame layout - waitForIdleSync(); - assertNotNull(mAuthContainer.mCredentialView); - verify(mAuthContainer.mFrameLayout).addView(eq(mAuthContainer.mCredentialView)); - } - - @Test - public void testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() { - initializeContainer( - Authenticators.BIOMETRIC_WEAK | Authenticators.DEVICE_CREDENTIAL); - - mAuthContainer.mBiometricView = mock(AuthBiometricView.class); - mAuthContainer.animateToCredentialUI(); - verify(mAuthContainer.mBiometricView).startTransitionToCredentialUI(); - } - - @Test - public void testShowBiometricUI() { - initializeContainer(Authenticators.BIOMETRIC_WEAK); - - assertNotEquals(null, mAuthContainer.mBiometricView); - - mAuthContainer.onAttachedToWindowInternal(); - verify(mAuthContainer.mBiometricScrollView).addView(mAuthContainer.mBiometricView); - // Credential view is not added - verify(mAuthContainer.mFrameLayout, never()).addView(any()); - } - - @Test - public void testShowCredentialUI_doesNotInflateBiometricUI() { - initializeContainer(Authenticators.DEVICE_CREDENTIAL); - - mAuthContainer.onAttachedToWindowInternal(); - - assertNull(null, mAuthContainer.mBiometricView); - assertNotNull(mAuthContainer.mCredentialView); - verify(mAuthContainer.mFrameLayout).addView(mAuthContainer.mCredentialView); - } - - @Test - public void testCredentialViewUsesEffectiveUserId() { - final int dummyEffectiveUserId = 200; - when(mUserManager.getCredentialOwnerProfile(anyInt())).thenReturn(dummyEffectiveUserId); - - initializeContainer(Authenticators.DEVICE_CREDENTIAL); - mAuthContainer.onAttachedToWindowInternal(); - assertTrue(mAuthContainer.mCredentialView instanceof AuthCredentialPatternView); - assertEquals(dummyEffectiveUserId, mAuthContainer.mCredentialView.mEffectiveUserId); - assertEquals(Utils.CREDENTIAL_PATTERN, mAuthContainer.mCredentialView.mCredentialType); - } - - @Test - public void testCredentialUI_disablesClickingOnBackground() { - // In the credential view, clicking on the background (to cancel authentication) is not - // valid. Thus, the listener should be null, and it should not be in the accessibility - // hierarchy. - initializeContainer(Authenticators.DEVICE_CREDENTIAL); - - mAuthContainer.onAttachedToWindowInternal(); - - verify(mAuthContainer.mBackgroundView).setOnClickListener(eq(null)); - verify(mAuthContainer.mBackgroundView).setImportantForAccessibility( - eq(View.IMPORTANT_FOR_ACCESSIBILITY_NO)); - } - - @Test - public void testOnDialogAnimatedIn_sendsCancelReason_whenPendingDismiss() { - initializeContainer(Authenticators.BIOMETRIC_WEAK); - mAuthContainer.mContainerState = AuthContainerView.STATE_PENDING_DISMISS; - mAuthContainer.onDialogAnimatedIn(); - verify(mCallback).onDismissed( - eq(AuthDialogCallback.DISMISSED_USER_CANCELED), - eq(null) /* credentialAttestation */); - } - - @Test - public void testLayoutParams_hasSecureWindowFlag() { - final IBinder windowToken = mock(IBinder.class); - final WindowManager.LayoutParams layoutParams = - AuthContainerView.getLayoutParams(windowToken, ""); - assertTrue((layoutParams.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0); - } - - @Test - public void testLayoutParams_excludesImeInsets() { - final IBinder windowToken = mock(IBinder.class); - final WindowManager.LayoutParams layoutParams = - AuthContainerView.getLayoutParams(windowToken, ""); - assertTrue((layoutParams.getFitInsetsTypes() & WindowInsets.Type.ime()) == 0); - } - - private void initializeContainer(int authenticators) { - AuthContainerView.Config config = new AuthContainerView.Config(); - config.mContext = mContext; - config.mCallback = mCallback; - config.mSensorIds = new int[] {0}; - config.mCredentialAllowed = false; - - PromptInfo promptInfo = new PromptInfo(); - promptInfo.setAuthenticators(authenticators); - config.mPromptInfo = promptInfo; - - final List<FingerprintSensorPropertiesInternal> fpProps = new ArrayList<>(); - - final List<ComponentInfoInternal> componentInfo = new ArrayList<>(); - componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */)); - componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */)); - - fpProps.add(new FingerprintSensorPropertiesInternal(0, - SensorProperties.STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_REAR, - false /* resetLockoutRequiresHardwareAuthToken */)); - mAuthContainer = new TestableAuthContainer(config, fpProps, null /* faceProps */, - mWakefulnessLifecycle); - } - - private class TestableAuthContainer extends AuthContainerView { - TestableAuthContainer(AuthContainerView.Config config, - @Nullable List<FingerprintSensorPropertiesInternal> fpProps, - @Nullable List<FaceSensorPropertiesInternal> faceProps, - WakefulnessLifecycle wakefulnessLifecycle) { - - super(config, new MockInjector(), fpProps, faceProps, wakefulnessLifecycle); - } - - @Override - public void animateAway(int reason) { - // TODO: Credential attestation should be testable/tested - mConfig.mCallback.onDismissed(reason, null /* credentialAttestation */); - } - } - - private final class MockInjector extends AuthContainerView.Injector { - @Override - public ScrollView getBiometricScrollView(FrameLayout parent) { - return mock(ScrollView.class); - } - - @Override - public FrameLayout inflateContainerView(LayoutInflater factory, ViewGroup root) { - return mock(FrameLayout.class); - } - - @Override - public AuthPanelController getPanelController(Context context, View view) { - return mock(AuthPanelController.class); - } - - @Override - public ImageView getBackgroundView(FrameLayout parent) { - return mock(ImageView.class); - } - - @Override - public View getPanelView(FrameLayout parent) { - return mock(View.class); - } - - @Override - public int getAnimateCredentialStartDelayMs() { - return 0; - } - - @Override - public UserManager getUserManager(Context context) { - return mUserManager; - } - - @Override - public @Utils.CredentialType int getCredentialType(Context context, int effectiveUserId) { - return Utils.CREDENTIAL_PATTERN; - } - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt new file mode 100644 index 000000000000..6f0a8a6adfef --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -0,0 +1,324 @@ +/* + * 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.app.admin.DevicePolicyManager +import android.hardware.biometrics.BiometricConstants +import android.hardware.biometrics.BiometricManager +import android.hardware.biometrics.ComponentInfoInternal +import android.hardware.biometrics.PromptInfo +import android.hardware.biometrics.SensorProperties +import android.hardware.face.FaceSensorPropertiesInternal +import android.hardware.fingerprint.FingerprintSensorProperties +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal +import android.os.Handler +import android.os.IBinder +import android.os.UserManager +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.testing.ViewUtils +import android.view.View +import android.view.WindowInsets +import android.view.WindowManager +import android.widget.ScrollView +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.WakefulnessLifecycle +import com.google.common.truth.Truth.assertThat +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.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.Mockito.`when` as whenever + +@Ignore +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +@SmallTest +class AuthContainerViewTest : SysuiTestCase() { + + @JvmField @Rule + var rule = MockitoJUnit.rule() + + @Mock + lateinit var callback: AuthDialogCallback + @Mock + lateinit var userManager: UserManager + @Mock + lateinit var lockPatternUtils: LockPatternUtils + @Mock + lateinit var wakefulnessLifecycle: WakefulnessLifecycle + @Mock + lateinit var windowToken: IBinder + + private lateinit var authContainer: TestAuthContainerView + + @Test + fun testActionAuthenticated_sendsDismissedAuthenticated() { + initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK) + authContainer.mBiometricCallback.onAction( + AuthBiometricView.Callback.ACTION_AUTHENTICATED + ) + waitForIdleSync() + + verify(callback).onDismissed( + eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED), + eq<ByteArray?>(null) /* credentialAttestation */ + ) + assertThat(authContainer.parent).isNull() + } + + @Test + fun testActionUserCanceled_sendsDismissedUserCanceled() { + initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK) + authContainer.mBiometricCallback.onAction( + AuthBiometricView.Callback.ACTION_USER_CANCELED + ) + waitForIdleSync() + + verify(callback).onSystemEvent( + eq(BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL) + ) + verify(callback).onDismissed( + eq(AuthDialogCallback.DISMISSED_USER_CANCELED), + eq<ByteArray?>(null) /* credentialAttestation */ + ) + assertThat(authContainer.parent).isNull() + } + + @Test + fun testActionButtonNegative_sendsDismissedButtonNegative() { + initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK) + authContainer.mBiometricCallback.onAction( + AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE + ) + waitForIdleSync() + + verify(callback).onDismissed( + eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE), + eq<ByteArray?>(null) /* credentialAttestation */ + ) + assertThat(authContainer.parent).isNull() + } + + @Test + fun testActionTryAgain_sendsTryAgain() { + initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK) + authContainer.mBiometricCallback.onAction( + AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN + ) + waitForIdleSync() + + verify(callback).onTryAgainPressed() + } + + @Test + fun testActionError_sendsDismissedError() { + initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK) + authContainer.mBiometricCallback.onAction( + AuthBiometricView.Callback.ACTION_ERROR + ) + waitForIdleSync() + + verify(callback).onDismissed( + eq(AuthDialogCallback.DISMISSED_ERROR), + eq<ByteArray?>(null) /* credentialAttestation */ + ) + assertThat(authContainer.parent).isNull() + } + + @Test + fun testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() { + initializeContainer( + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + authContainer.mBiometricCallback.onAction( + AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL + ) + waitForIdleSync() + + verify(callback).onDeviceCredentialPressed() + assertThat(authContainer.hasCredentialView()).isTrue() + } + + @Test + fun testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() { + initializeContainer( + BiometricManager.Authenticators.BIOMETRIC_WEAK or + BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) + authContainer.animateToCredentialUI() + waitForIdleSync() + + assertThat(authContainer.hasCredentialView()).isTrue() + } + + @Test + fun testShowBiometricUI() { + initializeContainer(BiometricManager.Authenticators.BIOMETRIC_WEAK) + + waitForIdleSync() + + assertThat(authContainer.hasCredentialView()).isFalse() + assertThat(authContainer.hasBiometricPrompt()).isTrue() + } + + @Test + fun testShowCredentialUI() { + initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + waitForIdleSync() + + assertThat(authContainer.hasCredentialView()).isTrue() + assertThat(authContainer.hasBiometricPrompt()).isFalse() + } + + @Test + fun testCredentialViewUsesEffectiveUserId() { + whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(200) + whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(200))).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING + ) + + initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + waitForIdleSync() + + assertThat(authContainer.hasCredentialPatternView()).isTrue() + assertThat(authContainer.hasBiometricPrompt()).isFalse() + } + + @Test + fun testCredentialUI_disablesClickingOnBackground() { + whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20) + whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))).thenReturn( + DevicePolicyManager.PASSWORD_QUALITY_NUMERIC + ) + + // In the credential view, clicking on the background (to cancel authentication) is not + // valid. Thus, the listener should be null, and it should not be in the accessibility + // hierarchy. + initializeContainer(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + waitForIdleSync() + + assertThat(authContainer.hasCredentialPasswordView()).isTrue() + assertThat(authContainer.hasBiometricPrompt()).isFalse() + assertThat( + authContainer.findViewById<View>(R.id.background)?.isImportantForAccessibility + ).isFalse() + + authContainer.findViewById<View>(R.id.background)?.performClick() + waitForIdleSync() + + assertThat(authContainer.hasCredentialPasswordView()).isTrue() + assertThat(authContainer.hasBiometricPrompt()).isFalse() + } + + @Test + fun testLayoutParams_hasSecureWindowFlag() { + val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") + assertThat((layoutParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0).isTrue() + } + + @Test + fun testLayoutParams_excludesImeInsets() { + val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") + assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.ime()) == 0).isTrue() + } + + private fun initializeContainer(authenticators: Int) { + val config = AuthContainerView.Config() + config.mContext = mContext + config.mCallback = callback + config.mSensorIds = intArrayOf(0) + config.mSkipAnimation = true + config.mPromptInfo = PromptInfo() + config.mPromptInfo.authenticators = authenticators + val componentInfo = listOf( + ComponentInfoInternal( + "faceSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, "" /* softwareVersion */ + ), + ComponentInfoInternal( + "matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */ + ) + ) + val fpProps = listOf( + FingerprintSensorPropertiesInternal( + 0, + SensorProperties.STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, + componentInfo, + FingerprintSensorProperties.TYPE_REAR, + false /* resetLockoutRequiresHardwareAuthToken */ + ) + ) + authContainer = TestAuthContainerView( + config, + fpProps, + listOf(), + wakefulnessLifecycle, + userManager, + lockPatternUtils, + Handler(TestableLooper.get(this).looper) + ) + ViewUtils.attachView(authContainer) + } + + private inner class TestAuthContainerView( + config: Config, + fpProps: List<FingerprintSensorPropertiesInternal>, + faceProps: List<FaceSensorPropertiesInternal>, + wakefulnessLifecycle: WakefulnessLifecycle, + userManager: UserManager, + lockPatternUtils: LockPatternUtils, + mainHandler: Handler + ) : AuthContainerView( + config, fpProps, faceProps, + wakefulnessLifecycle, userManager, lockPatternUtils, mainHandler + ) { + override fun postOnAnimation(runnable: Runnable) { + runnable.run() + } + } + + override fun waitForIdleSync() { + TestableLooper.get(this).processAllMessages() + super.waitForIdleSync() + } +} + +private fun AuthContainerView.hasBiometricPrompt() = + (findViewById<ScrollView>(R.id.biometric_scrollview)?.childCount ?: 0) > 0 + +private fun AuthContainerView.hasCredentialView() = + hasCredentialPatternView() || hasCredentialPasswordView() + +private fun AuthContainerView.hasCredentialPatternView() = + findViewById<View>(R.id.lockPattern) != null + +private fun AuthContainerView.hasCredentialPasswordView() = + findViewById<View>(R.id.lockPassword) != null 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 c37e966f3540..cfac96512582 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -16,8 +16,9 @@ package com.android.systemui.biometrics; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.Authenticators; -import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNull; @@ -62,6 +63,7 @@ import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; +import android.os.UserManager; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; @@ -71,6 +73,7 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.internal.widget.LockPatternUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -79,6 +82,8 @@ import com.android.systemui.util.concurrency.Execution; import com.android.systemui.util.concurrency.FakeExecution; import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.AdditionalMatchers; @@ -86,7 +91,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InOrder; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; @@ -94,11 +100,15 @@ import java.util.Random; import javax.inject.Provider; +@Ignore @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest public class AuthControllerTest extends SysuiTestCase { + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Mock private PackageManager mPackageManager; @Mock @@ -128,6 +138,10 @@ public class AuthControllerTest extends SysuiTestCase { @Mock private WakefulnessLifecycle mWakefulnessLifecycle; @Mock + private UserManager mUserManager; + @Mock + private LockPatternUtils mLockPatternUtils; + @Mock private StatusBarStateController mStatusBarStateController; @Captor ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mAuthenticatorsRegisteredCaptor; @@ -144,8 +158,6 @@ public class AuthControllerTest extends SysuiTestCase { @Before public void setup() throws RemoteException { - MockitoAnnotations.initMocks(this); - mContextSpy = spy(mContext); mExecution = new FakeExecution(); mTestableLooper = TestableLooper.get(this); @@ -343,8 +355,8 @@ public class AuthControllerTest extends SysuiTestCase { @Test public void testOnAuthenticationSucceededInvoked_whenSystemRequested() { showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); - mAuthController.onBiometricAuthenticated(); - verify(mDialog1).onAuthenticationSucceeded(); + mAuthController.onBiometricAuthenticated(TYPE_FINGERPRINT); + verify(mDialog1).onAuthenticationSucceeded(eq(TYPE_FINGERPRINT)); } @Test @@ -528,8 +540,7 @@ public class AuthControllerTest extends SysuiTestCase { doAnswer(invocation -> { Object[] args = invocation.getArguments(); Bundle savedState = (Bundle) args[0]; - savedState.putInt( - AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING); + savedState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false); return null; // onSaveState returns void }).when(mDialog1).onSaveState(any()); @@ -558,8 +569,7 @@ public class AuthControllerTest extends SysuiTestCase { doAnswer(invocation -> { Object[] args = invocation.getArguments(); Bundle savedState = (Bundle) args[0]; - savedState.putInt( - AuthDialog.KEY_CONTAINER_STATE, AuthContainerView.STATE_SHOWING); + savedState.putBoolean(AuthDialog.KEY_CONTAINER_GOING_AWAY, false); savedState.putBoolean(AuthDialog.KEY_CREDENTIAL_SHOWING, true); return null; // onSaveState returns void }).when(mDialog1).onSaveState(any()); @@ -697,7 +707,7 @@ public class AuthControllerTest extends SysuiTestCase { 0 /* operationId */, "testPackage", 1 /* requestId */, - BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT); + BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE); } private PromptInfo createTestPromptInfo() { @@ -739,15 +749,16 @@ public class AuthControllerTest extends SysuiTestCase { super(context, execution, commandQueue, activityTaskManager, windowManager, fingerprintManager, faceManager, udfpsControllerFactory, sidefpsControllerFactory, mDisplayManager, mWakefulnessLifecycle, - statusBarStateController, mHandler); + mUserManager, mLockPatternUtils, statusBarStateController, mHandler); } @Override protected AuthDialog buildDialog(PromptInfo promptInfo, - boolean requireConfirmation, int userId, int[] sensorIds, boolean credentialAllowed, + boolean requireConfirmation, int userId, int[] sensorIds, String opPackageName, boolean skipIntro, long operationId, long requestId, @BiometricManager.BiometricMultiSensorMode int multiSensorConfig, - WakefulnessLifecycle wakefulnessLifecycle) { + WakefulnessLifecycle wakefulnessLifecycle, UserManager userManager, + LockPatternUtils lockPatternUtils) { mLastBiometricPromptInfo = promptInfo; diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt index 254fc5945522..839c0ab1318f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt @@ -17,10 +17,10 @@ package com.android.systemui.biometrics import android.animation.Animator -import android.graphics.Insets import android.app.ActivityManager import android.app.ActivityTaskManager import android.content.ComponentName +import android.graphics.Insets import android.graphics.Rect import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS @@ -65,8 +65,8 @@ import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.any import org.mockito.Mockito.anyFloat -import org.mockito.Mockito.anyLong import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyLong import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index 066a866118dd..ef82c3ec3322 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -57,9 +57,9 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit +import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidTestingRunner::class) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt index 2cd470e49d0c..3d8d1282e184 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt @@ -38,11 +38,11 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.anyInt -import org.mockito.Mockito.nullable import org.mockito.Mockito.never -import org.mockito.Mockito.`when` as whenever +import org.mockito.Mockito.nullable import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit +import org.mockito.Mockito.`when` as whenever private const val DISPLAY_ID = "" // default display id private const val SENSOR_X = 50 diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index 2860b50c2295..b3d54590dc99 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -28,6 +28,7 @@ import android.service.dreams.DreamService; import android.service.dreams.IDreamOverlay; import android.service.dreams.IDreamOverlayCallback; import android.testing.AndroidTestingRunner; +import android.view.ViewGroup; import android.view.WindowManager; import android.view.WindowManagerImpl; @@ -99,6 +100,9 @@ public class DreamOverlayServiceTest extends SysuiTestCase { @Mock DreamPreviewComplication mPreviewComplication; + @Mock + ViewGroup mDreamOverlayContainerViewParent; + DreamOverlayService mService; @Before @@ -152,6 +156,23 @@ public class DreamOverlayServiceTest extends SysuiTestCase { } @Test + public void testDreamOverlayContainerViewRemovedFromOldParentWhenInitialized() + throws Exception { + when(mDreamOverlayContainerView.getParent()) + .thenReturn(mDreamOverlayContainerViewParent) + .thenReturn(null); + + final IBinder proxy = mService.onBind(new Intent()); + final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); + + // Inform the overlay service of dream starting. + overlay.startDream(mWindowParams, mDreamOverlayCallback); + mMainExecutor.runAllReady(); + + verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView); + } + + @Test public void testShouldShowComplicationsFalseByDefault() { mService.onBind(new Intent()); 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 6c29ecc7ae50..11f76a381ad4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -448,9 +448,10 @@ public class CommandQueueTest extends SysuiTestCase { @Test public void testOnBiometricAuthenticated() { - mCommandQueue.onBiometricAuthenticated(); + final int id = 12; + mCommandQueue.onBiometricAuthenticated(id); waitForIdleSync(); - verify(mCallbacks).onBiometricAuthenticated(); + verify(mCallbacks).onBiometricAuthenticated(eq(id)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java index 466d954e7be0..3c1a73eb672e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -45,7 +45,6 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -66,15 +65,17 @@ import android.os.BatteryManager; import android.os.Looper; import android.os.RemoteException; import android.os.UserManager; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; import android.view.ViewGroup; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.app.IBatteryStats; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; @@ -106,7 +107,8 @@ import java.text.NumberFormat; import java.util.Collections; @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper public class KeyguardIndicationControllerTest extends SysuiTestCase { private static final String ORGANIZATION_NAME = "organization"; @@ -164,11 +166,15 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Captor private ArgumentCaptor<KeyguardIndication> mKeyguardIndicationCaptor; @Captor + private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor; + @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateControllerCallbackCaptor; private KeyguardStateController.Callback mKeyguardStateControllerCallback; + private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; private StatusBarStateController.StateListener mStatusBarStateListener; private BroadcastReceiver mBroadcastReceiver; private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); + private TestableLooper mTestableLooper; private KeyguardIndicationTextView mTextView; // AOD text @@ -181,6 +187,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mTestableLooper = TestableLooper.get(this); mTextView = new KeyguardIndicationTextView(mContext); mTextView.setAnimationsEnabled(false); @@ -226,7 +233,10 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { Looper.prepare(); } - mController = new KeyguardIndicationController(mContext, mWakeLockBuilder, + mController = new KeyguardIndicationController( + mContext, + mTestableLooper.getLooper(), + mWakeLockBuilder, mKeyguardStateController, mStatusBarStateController, mKeyguardUpdateMonitor, mDockManager, mBroadcastDispatcher, mDevicePolicyManager, mIBatteryStats, mUserManager, mExecutor, mExecutor, mFalsingManager, mLockPatternUtils, @@ -245,6 +255,10 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mKeyguardStateControllerCallbackCaptor.capture()); mKeyguardStateControllerCallback = mKeyguardStateControllerCallbackCaptor.getValue(); + verify(mKeyguardUpdateMonitor).registerCallback( + mKeyguardUpdateMonitorCallbackCaptor.capture()); + mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); + mExecutor.runAllReady(); reset(mRotateTextViewController); } @@ -267,7 +281,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mAlignmentListener.getValue().onAlignmentStateChanged(DockManager.ALIGN_STATE_POOR); }); mInstrumentation.waitForIdleSync(); - + mTestableLooper.processAllMessages(); verifyIndicationMessage(INDICATION_TYPE_ALIGNMENT, mContext.getResources().getString(R.string.dock_alignment_slow_charging)); @@ -285,6 +299,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mAlignmentListener.getValue().onAlignmentStateChanged(DockManager.ALIGN_STATE_TERRIBLE); }); mInstrumentation.waitForIdleSync(); + mTestableLooper.processAllMessages(); verifyIndicationMessage(INDICATION_TYPE_ALIGNMENT, mContext.getResources().getString(R.string.dock_alignment_not_charging)); @@ -303,6 +318,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mAlignmentListener.getValue().onAlignmentStateChanged(DockManager.ALIGN_STATE_POOR); }); mInstrumentation.waitForIdleSync(); + mTestableLooper.processAllMessages(); assertThat(mTextView.getText()).isEqualTo( mContext.getResources().getString(R.string.dock_alignment_slow_charging)); @@ -321,6 +337,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mAlignmentListener.getValue().onAlignmentStateChanged(DockManager.ALIGN_STATE_TERRIBLE); }); mInstrumentation.waitForIdleSync(); + mTestableLooper.processAllMessages(); assertThat(mTextView.getText()).isEqualTo( mContext.getResources().getString(R.string.dock_alignment_not_charging)); @@ -331,9 +348,12 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void disclosure_unmanaged() { createController(); + mController.setVisible(true); when(mKeyguardStateController.isShowing()).thenReturn(true); when(mDevicePolicyManager.isDeviceManaged()).thenReturn(false); when(mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()).thenReturn(false); + reset(mRotateTextViewController); + sendUpdateDisclosureBroadcast(); mExecutor.runAllReady(); @@ -347,6 +367,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { when(mDevicePolicyManager.isDeviceManaged()).thenReturn(true); when(mDevicePolicyManager.getDeviceOwnerOrganizationName()).thenReturn(null); sendUpdateDisclosureBroadcast(); + mController.setVisible(true); mExecutor.runAllReady(); verifyIndicationMessage(INDICATION_TYPE_DISCLOSURE, mDisclosureGeneric); @@ -355,6 +376,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void disclosure_orgOwnedDeviceWithManagedProfile_noOrganizationName() { createController(); + mController.setVisible(true); when(mKeyguardStateController.isShowing()).thenReturn(true); when(mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()).thenReturn(true); when(mUserManager.getProfiles(anyInt())).thenReturn(Collections.singletonList( @@ -369,6 +391,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void disclosure_deviceOwner_withOrganizationName() { createController(); + mController.setVisible(true); when(mKeyguardStateController.isShowing()).thenReturn(true); when(mDevicePolicyManager.isDeviceManaged()).thenReturn(true); when(mDevicePolicyManager.getDeviceOwnerOrganizationName()).thenReturn(ORGANIZATION_NAME); @@ -381,6 +404,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void disclosure_orgOwnedDeviceWithManagedProfile_withOrganizationName() { createController(); + mController.setVisible(true); when(mKeyguardStateController.isShowing()).thenReturn(true); when(mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()).thenReturn(true); when(mUserManager.getProfiles(anyInt())).thenReturn(Collections.singletonList( @@ -397,6 +421,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { when(mKeyguardStateController.isShowing()).thenReturn(true); when(mDevicePolicyManager.isDeviceManaged()).thenReturn(false); createController(); + mController.setVisible(true); when(mDevicePolicyManager.isDeviceManaged()).thenReturn(true); when(mDevicePolicyManager.getDeviceOwnerOrganizationName()).thenReturn(null); @@ -424,7 +449,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void disclosure_deviceOwner_financedDeviceWithOrganizationName() { createController(); - + mController.setVisible(true); when(mKeyguardStateController.isShowing()).thenReturn(true); when(mDevicePolicyManager.isDeviceManaged()).thenReturn(true); when(mDevicePolicyManager.getDeviceOwnerOrganizationName()).thenReturn(ORGANIZATION_NAME); @@ -432,6 +457,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { .thenReturn(DEVICE_OWNER_TYPE_FINANCED); sendUpdateDisclosureBroadcast(); mExecutor.runAllReady(); + mController.setVisible(true); verifyIndicationMessage(INDICATION_TYPE_DISCLOSURE, mFinancedDisclosureWithOrganization); } @@ -469,10 +495,10 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void transientIndication_visibleWhenDozing() { createController(); - mController.setVisible(true); - mController.showTransientIndication(TEST_STRING_RES); + mStatusBarStateListener.onDozingChanged(true); + mController.showTransientIndication(TEST_STRING_RES); assertThat(mTextView.getText()).isEqualTo( mContext.getResources().getString(TEST_STRING_RES)); @@ -493,7 +519,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { reset(mRotateTextViewController); mStatusBarStateListener.onDozingChanged(true); - verifyHideIndication(INDICATION_TYPE_BIOMETRIC_MESSAGE); + assertThat(mTextView.getText()).isNotEqualTo(message); } @Test @@ -604,10 +630,9 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { } @Test - public void updateMonitor_listener() { + public void registersKeyguardStateCallback() { createController(); verify(mKeyguardStateController).addCallback(any()); - verify(mKeyguardUpdateMonitor, times(2)).registerCallback(any()); } @Test @@ -695,13 +720,13 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void onRefreshBatteryInfo_dozing_dischargingWithOverheat_presentBatteryPercentage() { createController(); + mController.setVisible(true); BatteryStatus status = new BatteryStatus(BatteryManager.BATTERY_STATUS_DISCHARGING, 90 /* level */, 0 /* plugged */, BatteryManager.BATTERY_HEALTH_OVERHEAT, 0 /* maxChargingWattage */, true /* present */); mController.getKeyguardCallback().onRefreshBatteryInfo(status); mStatusBarStateListener.onDozingChanged(true); - mController.setVisible(true); String percentage = NumberFormat.getPercentInstance().format(90 / 100f); assertThat(mTextView.getText()).isEqualTo(percentage); @@ -710,9 +735,9 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void onRequireUnlockForNfc_showsRequireUnlockForNfcIndication() { createController(); + mController.setVisible(true); String message = mContext.getString(R.string.require_unlock_for_nfc); mController.getKeyguardCallback().onRequireUnlockForNfc(); - mController.setVisible(true); verifyTransientMessage(message); } @@ -778,6 +803,9 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void testOnKeyguardShowingChanged_showing_updatesPersistentMessages() { createController(); + mController.setVisible(true); + mExecutor.runAllReady(); + reset(mRotateTextViewController); // GIVEN keyguard is showing when(mKeyguardStateController.isShowing()).thenReturn(true); @@ -799,6 +827,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void onTrustGrantedMessageDoesNotShowUntilTrustGranted() { createController(); + mController.setVisible(true); + reset(mRotateTextViewController); // GIVEN a trust granted message but trust isn't granted final String trustGrantedMsg = "testing trust granted message"; @@ -808,7 +838,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { // WHEN trust is granted when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true); - mController.setVisible(true); + mKeyguardUpdateMonitorCallback.onTrustChanged(KeyguardUpdateMonitor.getCurrentUser()); // THEN verify the trust granted message shows verifyIndicationMessage( @@ -819,6 +849,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Test public void onTrustGrantedMessageDoesShowsOnTrustGranted() { createController(); + mController.setVisible(true); // GIVEN trust is granted when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java index 077b41a0aa90..5f2bbd341962 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java @@ -126,12 +126,6 @@ public class DozeParametersTest extends SysuiTestCase { setAodEnabledForTest(true); setShouldControlUnlockedScreenOffForTest(true); setDisplayNeedsBlankingForTest(false); - - // Default to false here (with one test to make sure that when it returns true, we respect - // that). We'll test the specific conditions for this to return true/false in the - // UnlockedScreenOffAnimationController's tests. - when(mUnlockedScreenOffAnimationController.shouldPlayUnlockedScreenOffAnimation()) - .thenReturn(false); } @Test @@ -180,12 +174,9 @@ public class DozeParametersTest extends SysuiTestCase { */ @Test public void testControlUnlockedScreenOffAnimation_dozeAfterScreenOff_false() { - mDozeParameters.mKeyguardVisibilityCallback.onKeyguardVisibilityChanged(true); - // If AOD is disabled, we shouldn't want to control screen off. Also, let's double check // that when that value is updated, we called through to PowerManager. setAodEnabledForTest(false); - assertFalse(mDozeParameters.shouldControlScreenOff()); assertTrue(mPowerManagerDozeAfterScreenOff); @@ -197,6 +188,7 @@ public class DozeParametersTest extends SysuiTestCase { @Test public void testControlUnlockedScreenOffAnimationDisabled_dozeAfterScreenOff() { + setShouldControlUnlockedScreenOffForTest(true); when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(false); assertFalse(mDozeParameters.shouldControlUnlockedScreenOff()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt index 0936b773d4b3..050563a5707c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt @@ -31,7 +31,6 @@ import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.StatusBarStateControllerImpl import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.settings.GlobalSettings -import junit.framework.Assert.assertFalse import org.junit.After import org.junit.Before import org.junit.Test @@ -134,7 +133,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { */ @Test fun testAodUiShownIfNotInteractive() { - `when`(dozeParameters.canControlUnlockedScreenOff()).thenReturn(true) + `when`(dozeParameters.shouldControlUnlockedScreenOff()).thenReturn(true) `when`(powerManager.isInteractive).thenReturn(false) val callbackCaptor = ArgumentCaptor.forClass(Runnable::class.java) @@ -157,7 +156,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { */ @Test fun testAodUiNotShownIfInteractive() { - `when`(dozeParameters.canControlUnlockedScreenOff()).thenReturn(true) + `when`(dozeParameters.shouldControlUnlockedScreenOff()).thenReturn(true) `when`(powerManager.isInteractive).thenReturn(true) val callbackCaptor = ArgumentCaptor.forClass(Runnable::class.java) @@ -168,13 +167,4 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { verify(notificationPanelViewController, never()).showAodUi() } - - @Test - fun testNoAnimationPlaying_dozeParamsCanNotControlScreenOff() { - `when`(dozeParameters.canControlUnlockedScreenOff()).thenReturn(false) - - assertFalse(controller.shouldPlayUnlockedScreenOffAnimation()) - controller.startAnimation() - assertFalse(controller.isAnimationPlaying()) - } }
\ No newline at end of file diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 0e9926590511..0ea087d6de0f 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -2749,6 +2749,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } private void updateWindowMagnificationConnectionIfNeeded(AccessibilityUserState userState) { + if (!mMagnificationController.supportWindowMagnification()) { + return; + } final boolean connect = (userState.isShortcutMagnificationEnabledLocked() || userState.isDisplayMagnificationEnabledLocked()) && (userState.getMagnificationCapabilitiesLocked() diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java index a95820966926..fa32452f389e 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java @@ -303,7 +303,7 @@ public class FullScreenMagnificationController implements public void onImeWindowVisibilityChanged(boolean shown) { final Message m = PooledLambda.obtainMessage( FullScreenMagnificationController::notifyImeWindowVisibilityChanged, - FullScreenMagnificationController.this, shown); + FullScreenMagnificationController.this, mDisplayId, shown); mControllerCtx.getHandler().sendMessage(m); } @@ -1215,11 +1215,12 @@ public class FullScreenMagnificationController implements /** * Notifies that the IME window visibility changed. * + * @param displayId the logical display id * @param shown {@code true} means the IME window shows on the screen. Otherwise it's * hidden. */ - void notifyImeWindowVisibilityChanged(boolean shown) { - mMagnificationInfoChangedCallback.onImeWindowVisibilityChanged(shown); + void notifyImeWindowVisibilityChanged(int displayId, boolean shown) { + mMagnificationInfoChangedCallback.onImeWindowVisibilityChanged(displayId, shown); } /** @@ -1609,17 +1610,19 @@ public class FullScreenMagnificationController implements * Called when the state of the magnification activation is changed. * It is for the logging data of the magnification activation state. * - * @param displayId The logical display id. + * @param displayId the logical display id * @param activated {@code true} if the magnification is activated, otherwise {@code false}. */ void onFullScreenMagnificationActivationState(int displayId, boolean activated); /** * Called when the IME window visibility changed. + * + * @param displayId the logical display id * @param shown {@code true} means the IME window shows on the screen. Otherwise it's * hidden. */ - void onImeWindowVisibilityChanged(boolean shown); + void onImeWindowVisibilityChanged(int displayId, boolean shown); /** * Called when the magnification spec changed. diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java index 09e82c787c90..62ba0c821f4e 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java @@ -17,6 +17,7 @@ package com.android.server.accessibility.magnification; import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_WINDOW; +import static android.content.pm.PackageManager.FEATURE_WINDOW_MAGNIFICATION; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; @@ -38,6 +39,7 @@ import android.os.UserHandle; import android.provider.Settings; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.view.accessibility.MagnificationAnimationCallback; import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; @@ -91,6 +93,8 @@ public class MagnificationController implements WindowMagnificationManager.Callb private FullScreenMagnificationController mFullScreenMagnificationController; private WindowMagnificationManager mWindowMagnificationMgr; private int mMagnificationCapabilities = ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; + /** Whether the platform supports window magnification feature. */ + private final boolean mSupportWindowMagnification; @GuardedBy("mLock") private int mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_NONE; @@ -99,7 +103,7 @@ public class MagnificationController implements WindowMagnificationManager.Callb // Track the active user to reset the magnification and get the associated user settings. private @UserIdInt int mUserId = UserHandle.USER_SYSTEM; @GuardedBy("mLock") - private boolean mImeWindowVisible = false; + private final SparseBooleanArray mIsImeVisibleArray = new SparseBooleanArray(); private long mWindowModeEnabledTime = 0; private long mFullScreenModeEnabledTime = 0; @@ -129,6 +133,8 @@ public class MagnificationController implements WindowMagnificationManager.Callb mScaleProvider = scaleProvider; LocalServices.getService(WindowManagerInternal.class) .getAccessibilityController().setUiChangesForAccessibilityCallbacks(this); + mSupportWindowMagnification = context.getPackageManager().hasSystemFeature( + FEATURE_WINDOW_MAGNIFICATION); } @VisibleForTesting @@ -185,6 +191,11 @@ public class MagnificationController implements WindowMagnificationManager.Callb } } + /** Returns {@code true} if the platform supports window magnification feature. */ + public boolean supportWindowMagnification() { + return mSupportWindowMagnification; + } + /** * Transitions to the target Magnification mode with current center of the magnification mode * if it is available. @@ -377,7 +388,7 @@ public class MagnificationController implements WindowMagnificationManager.Callb setActivatedModeAndSwitchDelegate(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); mLastActivatedMode = mActivatedMode; } - logMagnificationModeWithImeOnIfNeeded(); + logMagnificationModeWithImeOnIfNeeded(displayId); disableFullScreenMagnificationIfNeeded(displayId); } else { logMagnificationUsageState(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, @@ -432,7 +443,7 @@ public class MagnificationController implements WindowMagnificationManager.Callb setActivatedModeAndSwitchDelegate(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); mLastActivatedMode = mActivatedMode; } - logMagnificationModeWithImeOnIfNeeded(); + logMagnificationModeWithImeOnIfNeeded(displayId); disableWindowMagnificationIfNeeded(displayId); } else { logMagnificationUsageState(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN, @@ -454,12 +465,12 @@ public class MagnificationController implements WindowMagnificationManager.Callb } @Override - public void onImeWindowVisibilityChanged(boolean shown) { + public void onImeWindowVisibilityChanged(int displayId, boolean shown) { synchronized (mLock) { - mImeWindowVisible = shown; + mIsImeVisibleArray.put(displayId, shown); } - getWindowMagnificationMgr().onImeWindowVisibilityChanged(shown); - logMagnificationModeWithImeOnIfNeeded(); + getWindowMagnificationMgr().onImeWindowVisibilityChanged(displayId, shown); + logMagnificationModeWithImeOnIfNeeded(displayId); } /** @@ -575,11 +586,12 @@ public class MagnificationController implements WindowMagnificationManager.Callb } } - private void logMagnificationModeWithImeOnIfNeeded() { + private void logMagnificationModeWithImeOnIfNeeded(int displayId) { final int mode; synchronized (mLock) { - if (!mImeWindowVisible || mActivatedMode == ACCESSIBILITY_MAGNIFICATION_MODE_NONE) { + if (!mIsImeVisibleArray.get(displayId, false) + || mActivatedMode == ACCESSIBILITY_MAGNIFICATION_MODE_NONE) { return; } mode = mActivatedMode; diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java index 175182c7a3bb..3e07b095fd29 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java @@ -396,6 +396,10 @@ public class MagnificationProcessor { dumpTrackingTypingFocusEnabledState(pw, displayId, config.getMode()); } + pw.append(" SupportWindowMagnification=" + + mController.supportWindowMagnification()).println(); + pw.append(" WindowMagnificationConnectionState=" + + mController.getWindowMagnificationMgr().getConnectionState()).println(); } private int getIdOfLastServiceToMagnify(int mode, int displayId) { diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java index 25dcc2aea41b..041eece5ce48 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapper.java @@ -133,6 +133,25 @@ class WindowMagnificationConnectionWrapper { return true; } + boolean moveWindowMagnifierToPosition(int displayId, float positionX, float positionY, + @Nullable MagnificationAnimationCallback callback) { + if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) { + mTrace.logTrace(TAG + ".moveWindowMagnifierToPosition", + FLAGS_WINDOW_MAGNIFICATION_CONNECTION, "displayId=" + displayId + + ";positionX=" + positionX + ";positionY=" + positionY); + } + try { + mConnection.moveWindowMagnifierToPosition(displayId, positionX, positionY, + transformToRemoteCallback(callback, mTrace)); + } catch (RemoteException e) { + if (DBG) { + Slog.e(TAG, "Error calling moveWindowMagnifierToPosition()", e); + } + return false; + } + return true; + } + boolean showMagnificationButton(int displayId, int magnificationMode) { if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) { mTrace.logTrace(TAG + ".showMagnificationButton", diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java index 89910eac06c5..aeb1112ede5f 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java @@ -36,8 +36,10 @@ import android.graphics.Region; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import android.os.SystemClock; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.view.MotionEvent; import android.view.accessibility.IWindowMagnificationConnection; import android.view.accessibility.IWindowMagnificationConnectionCallback; @@ -87,6 +89,30 @@ public class WindowMagnificationManager implements }) public @interface WindowPosition {} + /** Window magnification connection is connecting. */ + private static final int CONNECTING = 0; + /** Window magnification connection is connected. */ + private static final int CONNECTED = 1; + /** Window magnification connection is disconnecting. */ + private static final int DISCONNECTING = 2; + /** Window magnification connection is disconnected. */ + private static final int DISCONNECTED = 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"CONNECTION_STATE"}, value = { + CONNECTING, + CONNECTED, + DISCONNECTING, + DISCONNECTED + }) + private @interface ConnectionState { + } + + @ConnectionState + private int mConnectionState = DISCONNECTED; + + private static final int WAIT_CONNECTION_TIMEOUT_MILLIS = 100; + private final Object mLock; private final Context mContext; @VisibleForTesting @@ -99,6 +125,7 @@ public class WindowMagnificationManager implements private SparseArray<WindowMagnifier> mWindowMagnifiers = new SparseArray<>(); // Whether the following typing focus feature for magnification is enabled. private boolean mMagnificationFollowTypingEnabled = true; + private final SparseBooleanArray mIsImeVisibleArray = new SparseBooleanArray(); private boolean mReceiverRegistered = false; @VisibleForTesting @@ -178,7 +205,7 @@ public class WindowMagnificationManager implements */ public void setConnection(@Nullable IWindowMagnificationConnection connection) { if (DBG) { - Slog.d(TAG, "setConnection :" + connection); + Slog.d(TAG, "setConnection :" + connection + " ,mConnectionState=" + mConnectionState); } synchronized (mLock) { // Reset connectionWrapper. @@ -189,6 +216,13 @@ public class WindowMagnificationManager implements } mConnectionWrapper.unlinkToDeath(mConnectionCallback); mConnectionWrapper = null; + // The connection is still connecting so it is no need to reset the + // connection state to disconnected. + // TODO b/220086369 will reset the connection immediately when requestConnection + // is called + if (mConnectionState != CONNECTING) { + setConnectionState(DISCONNECTED); + } } if (connection != null) { mConnectionWrapper = new WindowMagnificationConnectionWrapper(connection, mTrace); @@ -199,9 +233,13 @@ public class WindowMagnificationManager implements mConnectionCallback = new ConnectionCallback(); mConnectionWrapper.linkToDeath(mConnectionCallback); mConnectionWrapper.setConnectionCallback(mConnectionCallback); + setConnectionState(CONNECTED); } catch (RemoteException e) { Slog.e(TAG, "setConnection failed", e); mConnectionWrapper = null; + setConnectionState(DISCONNECTED); + } finally { + mLock.notify(); } } } @@ -229,10 +267,20 @@ public class WindowMagnificationManager implements if (DBG) { Slog.d(TAG, "requestConnection :" + connect); } + if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) { + mTrace.logTrace(TAG + ".requestWindowMagnificationConnection", + FLAGS_WINDOW_MAGNIFICATION_CONNECTION, "connect=" + connect); + } synchronized (mLock) { - if (connect == isConnected()) { + if ((connect && (mConnectionState == CONNECTED || mConnectionState == CONNECTING)) + || (!connect && (mConnectionState == DISCONNECTED + || mConnectionState == DISCONNECTING))) { + Slog.w(TAG, + "requestConnection duplicated request: connect=" + connect + + " ,mConnectionState=" + mConnectionState); return false; } + if (connect) { final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF); if (!mReceiverRegistered) { @@ -247,19 +295,42 @@ public class WindowMagnificationManager implements } } } - if (mTrace.isA11yTracingEnabledForTypes(FLAGS_WINDOW_MAGNIFICATION_CONNECTION)) { - mTrace.logTrace(TAG + ".requestWindowMagnificationConnection", - FLAGS_WINDOW_MAGNIFICATION_CONNECTION, "connect=" + connect); + if (requestConnectionInternal(connect)) { + setConnectionState(connect ? CONNECTING : DISCONNECTING); + return true; + } else { + setConnectionState(DISCONNECTED); + return false; } + } + + private boolean requestConnectionInternal(boolean connect) { final long identity = Binder.clearCallingIdentity(); try { final StatusBarManagerInternal service = LocalServices.getService( StatusBarManagerInternal.class); - service.requestWindowMagnificationConnection(connect); + if (service != null) { + return service.requestWindowMagnificationConnection(connect); + } } finally { Binder.restoreCallingIdentity(identity); } - return true; + return false; + } + + /** + * Returns window magnification connection state. + */ + public int getConnectionState() { + return mConnectionState; + } + + private void setConnectionState(@ConnectionState int state) { + if (DBG) { + Slog.d(TAG, "setConnectionState : state=" + state + " ,mConnectionState=" + + mConnectionState); + } + mConnectionState = state; } /** @@ -314,9 +385,13 @@ public class WindowMagnificationManager implements float toCenterX = (float) (left + right) / 2; float toCenterY = (float) (top + bottom) / 2; - if (!isPositionInSourceBounds(displayId, toCenterX, toCenterY) - && isTrackingTypingFocusEnabled(displayId)) { - enableWindowMagnification(displayId, Float.NaN, toCenterX, toCenterY); + synchronized (mLock) { + if (mIsImeVisibleArray.get(displayId, false) + && !isPositionInSourceBounds(displayId, toCenterX, toCenterY) + && isTrackingTypingFocusEnabled(displayId)) { + moveWindowMagnifierToPositionInternal(displayId, toCenterX, toCenterY, + STUB_ANIMATION_CALLBACK); + } } } @@ -357,7 +432,7 @@ public class WindowMagnificationManager implements * @param displayId The logical display id. * @param trackingTypingFocusEnabled Enabled or disable the function of tracking typing focus. */ - private void setTrackingTypingFocusEnabled(int displayId, boolean trackingTypingFocusEnabled) { + void setTrackingTypingFocusEnabled(int displayId, boolean trackingTypingFocusEnabled) { synchronized (mLock) { WindowMagnifier magnifier = mWindowMagnifiers.get(displayId); if (magnifier == null) { @@ -384,7 +459,8 @@ public class WindowMagnificationManager implements * * @param shown {@code true} means the IME window shows on the screen. Otherwise, it's hidden. */ - void onImeWindowVisibilityChanged(boolean shown) { + void onImeWindowVisibilityChanged(int displayId, boolean shown) { + mIsImeVisibleArray.put(displayId, shown); if (shown) { enableAllTrackingTypingFocus(); } @@ -500,8 +576,11 @@ public class WindowMagnificationManager implements animationCallback, windowPosition, id); } - if (enabled && !previousEnabled) { - mCallback.onWindowMagnificationActivationState(displayId, true); + if (enabled) { + setTrackingTypingFocusEnabled(displayId, true); + if (!previousEnabled) { + mCallback.onWindowMagnificationActivationState(displayId, true); + } } return enabled; } @@ -563,14 +642,13 @@ public class WindowMagnificationManager implements } } + @GuardedBy("mLock") boolean isPositionInSourceBounds(int displayId, float x, float y) { - synchronized (mLock) { - WindowMagnifier magnifier = mWindowMagnifiers.get(displayId); - if (magnifier == null) { - return false; - } - return magnifier.isPositionInSourceBounds(x, y); + WindowMagnifier magnifier = mWindowMagnifiers.get(displayId); + if (magnifier == null) { + return false; } + return magnifier.isPositionInSourceBounds(x, y); } /** @@ -829,10 +907,10 @@ public class WindowMagnificationManager implements } @Override - public void onDrag(int displayId) { + public void onMove(int displayId) { if (mTrace.isA11yTracingEnabledForTypes( FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK)) { - mTrace.logTrace(TAG + "ConnectionCallback.onDrag", + mTrace.logTrace(TAG + "ConnectionCallback.onMove", FLAGS_WINDOW_MAGNIFICATION_CONNECTION_CALLBACK, "displayId=" + displayId); } @@ -849,6 +927,7 @@ public class WindowMagnificationManager implements mConnectionWrapper.unlinkToDeath(this); mConnectionWrapper = null; mConnectionCallback = null; + setConnectionState(DISCONNECTED); resetWindowMagnifiers(); } } @@ -884,7 +963,6 @@ public class WindowMagnificationManager implements mWindowMagnificationManager = windowMagnificationManager; } - @GuardedBy("mLock") boolean enableWindowMagnificationInternal(float scale, float centerX, float centerY, @Nullable MagnificationAnimationCallback animationCallback, @WindowPosition int windowPosition, int id) { @@ -962,7 +1040,6 @@ public class WindowMagnificationManager implements return mIdOfLastServiceToControl; } - @GuardedBy("mLock") int pointersInWindow(MotionEvent motionEvent) { int count = 0; final int pointerCount = motionEvent.getPointerCount(); @@ -1025,11 +1102,22 @@ public class WindowMagnificationManager implements float centerY, float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, MagnificationAnimationCallback animationCallback) { + // Wait for the connection with a timeout. + final long endMillis = SystemClock.uptimeMillis() + WAIT_CONNECTION_TIMEOUT_MILLIS; + while (mConnectionState == CONNECTING && (SystemClock.uptimeMillis() < endMillis)) { + try { + mLock.wait(endMillis - SystemClock.uptimeMillis()); + } catch (InterruptedException ie) { + /* ignore */ + } + } if (mConnectionWrapper == null) { - Slog.w(TAG, "enableWindowMagnificationInternal mConnectionWrapper is null"); + Slog.w(TAG, + "enableWindowMagnificationInternal mConnectionWrapper is null. " + + "mConnectionState=" + mConnectionState); return false; } - return mConnectionWrapper.enableWindowMagnification( + return mConnectionWrapper.enableWindowMagnification( displayId, scale, centerX, centerY, magnificationFrameOffsetRatioX, magnificationFrameOffsetRatioY, animationCallback); @@ -1050,8 +1138,16 @@ public class WindowMagnificationManager implements displayId, animationCallback); } + @GuardedBy("mLock") private boolean moveWindowMagnifierInternal(int displayId, float offsetX, float offsetY) { return mConnectionWrapper != null && mConnectionWrapper.moveWindowMagnifier( displayId, offsetX, offsetY); } + + @GuardedBy("mLock") + private boolean moveWindowMagnifierToPositionInternal(int displayId, float positionX, + float positionY, MagnificationAnimationCallback animationCallback) { + return mConnectionWrapper != null && mConnectionWrapper.moveWindowMagnifierToPosition( + displayId, positionX, positionY, animationCallback); + } } diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java index 111bd340a3d1..e6953f0032c7 100644 --- a/services/core/java/android/content/pm/PackageManagerInternal.java +++ b/services/core/java/android/content/pm/PackageManagerInternal.java @@ -657,6 +657,11 @@ public abstract class PackageManagerInternal { public abstract void notifyPackageUse(String packageName, int reason); /** + * Notify the package is force stopped. + */ + public abstract void onPackageProcessKilledForUninstall(String packageName); + + /** * Returns a package object for the given package name. */ public abstract @Nullable AndroidPackage getPackage(@NonNull String packageName); diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 6986d3bbe585..1f8ef8226c32 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -18,14 +18,22 @@ package com.android.server; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.ModuleInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; +import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; +import android.os.ServiceManager; import android.os.ShellCallback; import android.os.ShellCommand; import android.os.SystemProperties; @@ -42,6 +50,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; import java.util.stream.Collectors; /** @@ -49,6 +58,7 @@ import java.util.stream.Collectors; */ public class BinaryTransparencyService extends SystemService { private static final String TAG = "TransparencyService"; + private static final String EXTRA_SERVICE = "service"; @VisibleForTesting static final String VBMETA_DIGEST_UNINITIALIZED = "vbmeta-digest-uninitialized"; @@ -365,10 +375,80 @@ public class BinaryTransparencyService extends SystemService { // we are only interested in doing things at PHASE_BOOT_COMPLETED if (phase == PHASE_BOOT_COMPLETED) { - // due to potentially long computation that holds up boot time, apex sha computations - // are deferred to first call Slog.i(TAG, "Boot completed. Getting VBMeta Digest."); getVBMetaDigestInformation(); + + // due to potentially long computation that holds up boot time, computations for + // SHA256 digests of APEX and Module packages are scheduled here, + // but only executed when device is idle. + Slog.i(TAG, "Scheduling APEX and Module measurements to be updated."); + UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext, + BinaryTransparencyService.this); + } + } + + /** + * JobService to update binary measurements and update internal cache. + */ + public static class UpdateMeasurementsJobService extends JobService { + private static final int COMPUTE_APEX_MODULE_SHA256_JOB_ID = + BinaryTransparencyService.UpdateMeasurementsJobService.class.hashCode(); + + @Override + public boolean onStartJob(JobParameters params) { + Slog.d(TAG, "Job to update binary measurements started."); + if (params.getJobId() != COMPUTE_APEX_MODULE_SHA256_JOB_ID) { + return false; + } + + // we'll still update the measurements via threads to be mindful of low-end devices + // where this operation might take longer than expected, and so that we don't block + // system_server's main thread. + Executors.defaultThreadFactory().newThread(() -> { + // since we can't call updateBinaryMeasurements() directly, calling + // getApexInfo() achieves the same effect, and we simply discard the return + // value + + IBinder b = ServiceManager.getService(Context.BINARY_TRANSPARENCY_SERVICE); + IBinaryTransparencyService iBtsService = + IBinaryTransparencyService.Stub.asInterface(b); + try { + iBtsService.getApexInfo(); + } catch (RemoteException e) { + Slog.e(TAG, "Updating binary measurements was interrupted.", e); + return; + } + jobFinished(params, false); + }).start(); + + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + + @SuppressLint("DefaultLocale") + static void scheduleBinaryMeasurements(Context context, BinaryTransparencyService service) { + Slog.i(TAG, "Scheduling APEX & Module SHA256 digest computation job"); + final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + if (jobScheduler == null) { + Slog.e(TAG, "Failed to obtain an instance of JobScheduler."); + return; + } + + final JobInfo jobInfo = new JobInfo.Builder(COMPUTE_APEX_MODULE_SHA256_JOB_ID, + new ComponentName(context, UpdateMeasurementsJobService.class)) + .setRequiresDeviceIdle(true) + .build(); + if (jobScheduler.schedule(jobInfo) != JobScheduler.RESULT_SUCCESS) { + Slog.e(TAG, "Failed to schedule job to update binary measurements."); + return; + } + Slog.d(TAG, String.format( + "Job %d to update binary measurements scheduled successfully.", + COMPUTE_APEX_MODULE_SHA256_JOB_ID)); } } @@ -380,7 +460,7 @@ public class BinaryTransparencyService extends SystemService { @NonNull private List<PackageInfo> getInstalledApexs() { - List<PackageInfo> results = new ArrayList<PackageInfo>(); + List<PackageInfo> results = new ArrayList<>(); PackageManager pm = mContext.getPackageManager(); if (pm == null) { Slog.e(TAG, "Error obtaining an instance of PackageManager."); diff --git a/services/core/java/com/android/server/NetworkTimeUpdateService.java b/services/core/java/com/android/server/NetworkTimeUpdateService.java index fcde533fe05c..1e534b7c1515 100644 --- a/services/core/java/com/android/server/NetworkTimeUpdateService.java +++ b/services/core/java/com/android/server/NetworkTimeUpdateService.java @@ -36,12 +36,15 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.PowerManager; +import android.os.ResultReceiver; +import android.os.ShellCallback; import android.os.SystemClock; import android.os.TimestampedValue; import android.provider.Settings; import android.util.LocalLog; import android.util.Log; import android.util.NtpTrustedTime; +import android.util.NtpTrustedTime.TimeResult; import android.util.TimeUtils; import com.android.internal.util.DumpUtils; @@ -152,6 +155,42 @@ public class NetworkTimeUpdateService extends Binder { }, new IntentFilter(ACTION_POLL)); } + /** + * Clears the cached NTP time. For use during tests to simulate when no NTP time is available. + * + * <p>This operation takes place in the calling thread rather than the service's handler thread. + */ + void clearTimeForTests() { + mContext.enforceCallingPermission( + android.Manifest.permission.SET_TIME, "clear latest network time"); + + mTime.clearCachedTimeResult(); + + mLocalLog.log("clearTimeForTests"); + } + + /** + * Forces the service to refresh the NTP time. + * + * <p>This operation takes place in the calling thread rather than the service's handler thread. + * This method does not affect currently scheduled refreshes. If the NTP request is successful + * it will make an (asynchronously handled) suggestion to the time detector. + */ + boolean forceRefreshForTests() { + mContext.enforceCallingPermission( + android.Manifest.permission.SET_TIME, "force network time refresh"); + + boolean success = mTime.forceRefresh(); + mLocalLog.log("forceRefreshForTests: success=" + success); + + if (success) { + makeNetworkTimeSuggestion(mTime.getCachedTimeResult(), + "Origin: NetworkTimeUpdateService: forceRefreshForTests"); + } + + return success; + } + private void onPollNetworkTime(int event) { // If we don't have any default network, don't bother. if (mDefaultNetwork == null) return; @@ -193,12 +232,8 @@ public class NetworkTimeUpdateService extends Binder { resetAlarm(mPollingIntervalMs - cachedNtpResult.getAgeMillis(currentElapsedRealtimeMillis)); - // Suggest the time to the time detector. It may choose use it to set the system clock. - TimestampedValue<Long> timeSignal = new TimestampedValue<>( - cachedNtpResult.getElapsedRealtimeMillis(), cachedNtpResult.getTimeMillis()); - NetworkTimeSuggestion timeSuggestion = new NetworkTimeSuggestion(timeSignal); - timeSuggestion.addDebugInfo("Origin: NetworkTimeUpdateService. event=" + event); - mTimeDetector.suggestNetworkTime(timeSuggestion); + makeNetworkTimeSuggestion(cachedNtpResult, + "Origin: NetworkTimeUpdateService. event=" + event); } else { // No fresh fix; schedule retry mTryAgainCounter++; @@ -217,6 +252,15 @@ public class NetworkTimeUpdateService extends Binder { } } + /** Suggests the time to the time detector. It may choose use it to set the system clock. */ + private void makeNetworkTimeSuggestion(TimeResult ntpResult, String debugInfo) { + TimestampedValue<Long> timeSignal = new TimestampedValue<>( + ntpResult.getElapsedRealtimeMillis(), ntpResult.getTimeMillis()); + NetworkTimeSuggestion timeSuggestion = new NetworkTimeSuggestion(timeSignal); + timeSuggestion.addDebugInfo(debugInfo); + mTimeDetector.suggestNetworkTime(timeSuggestion); + } + /** * Cancel old alarm and starts a new one for the specified interval. * @@ -320,4 +364,11 @@ public class NetworkTimeUpdateService extends Binder { mLocalLog.dump(fd, pw, args); pw.println(); } + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { + new NetworkTimeUpdateServiceShellCommand(this).exec( + this, in, out, err, args, callback, resultReceiver); + } } diff --git a/services/core/java/com/android/server/NetworkTimeUpdateServiceShellCommand.java b/services/core/java/com/android/server/NetworkTimeUpdateServiceShellCommand.java new file mode 100644 index 000000000000..dc93023d82c5 --- /dev/null +++ b/services/core/java/com/android/server/NetworkTimeUpdateServiceShellCommand.java @@ -0,0 +1,90 @@ +/* + * 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.server; + +import android.annotation.NonNull; +import android.os.ShellCommand; + +import java.io.PrintWriter; +import java.util.Objects; + +/** Implements the shell command interface for {@link NetworkTimeUpdateService}. */ +class NetworkTimeUpdateServiceShellCommand extends ShellCommand { + + /** + * The name of the service. + */ + private static final String SHELL_COMMAND_SERVICE_NAME = "network_time_update_service"; + + /** + * A shell command that clears the time signal received from the network. + */ + private static final String SHELL_COMMAND_CLEAR_TIME = "clear_time"; + + /** + * A shell command that forces the time signal to be refreshed from the network. + */ + private static final String SHELL_COMMAND_FORCE_REFRESH = "force_refresh"; + + @NonNull + private final NetworkTimeUpdateService mNetworkTimeUpdateService; + + NetworkTimeUpdateServiceShellCommand(NetworkTimeUpdateService networkTimeUpdateService) { + mNetworkTimeUpdateService = Objects.requireNonNull(networkTimeUpdateService); + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + + switch (cmd) { + case SHELL_COMMAND_CLEAR_TIME: + return runClearTime(); + case SHELL_COMMAND_FORCE_REFRESH: + return runForceRefresh(); + default: { + return handleDefaultCommands(cmd); + } + } + } + + private int runClearTime() { + mNetworkTimeUpdateService.clearTimeForTests(); + return 0; + } + + private int runForceRefresh() { + boolean success = mNetworkTimeUpdateService.forceRefreshForTests(); + getOutPrintWriter().println(success); + return 0; + } + + @Override + public void onHelp() { + final PrintWriter pw = getOutPrintWriter(); + pw.printf("Network Time Update Service (%s) commands:\n", SHELL_COMMAND_SERVICE_NAME); + pw.printf(" help\n"); + pw.printf(" Print this help text.\n"); + pw.printf(" %s\n", SHELL_COMMAND_CLEAR_TIME); + pw.printf(" Clears the latest time.\n"); + pw.printf(" %s\n", SHELL_COMMAND_FORCE_REFRESH); + pw.printf(" Refreshes the latest time. Prints whether it was successful.\n"); + pw.println(); + } +} diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 38f6e6d9f165..d4ad718fbe73 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -4139,7 +4139,8 @@ public final class ActiveServices { final boolean isolated = (r.serviceInfo.flags&ServiceInfo.FLAG_ISOLATED_PROCESS) != 0; final String procName = r.processName; - HostingRecord hostingRecord = new HostingRecord("service", r.instanceName); + HostingRecord hostingRecord = new HostingRecord("service", r.instanceName, + r.definingPackageName, r.definingUid, r.serviceInfo.processName); ProcessRecord app; if (!isolated) { @@ -4177,11 +4178,12 @@ public final class ActiveServices { app = r.isolationHostProc; if (WebViewZygote.isMultiprocessEnabled() && r.serviceInfo.packageName.equals(WebViewZygote.getPackageName())) { - hostingRecord = HostingRecord.byWebviewZygote(r.instanceName); + hostingRecord = HostingRecord.byWebviewZygote(r.instanceName, r.definingPackageName, + r.definingUid, r.serviceInfo.processName); } if ((r.serviceInfo.flags & ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0) { hostingRecord = HostingRecord.byAppZygote(r.instanceName, r.definingPackageName, - r.definingUid); + r.definingUid, r.serviceInfo.processName); } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 3302cfb9bd66..47f9fd535801 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -12426,10 +12426,19 @@ public class ActivityManagerService extends IActivityManager.Stub } } - synchronized(this) { - return mServices.bindServiceLocked(caller, token, service, resolvedType, connection, - flags, instanceName, isSdkSandboxService, sdkSandboxClientdAppUid, - callingPackage, userId); + try { + if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + final ComponentName cn = service.getComponent(); + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindService:" + + (cn != null ? cn.toShortString() : service.getAction())); + } + synchronized (this) { + return mServices.bindServiceLocked(caller, token, service, resolvedType, connection, + flags, instanceName, isSdkSandboxService, sdkSandboxClientdAppUid, + callingPackage, userId); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } } @@ -13586,6 +13595,8 @@ public class ActivityManagerService extends IActivityManager.Stub intent.getIntExtra(Intent.EXTRA_UID, -1)), false, true, true, false, fullUninstall, userId, removed ? "pkg removed" : "pkg changed"); + getPackageManagerInternal() + .onPackageProcessKilledForUninstall(ssp); } else { // Kill any app zygotes always, since they can't fork new // processes with references to the old code diff --git a/services/core/java/com/android/server/am/HostingRecord.java b/services/core/java/com/android/server/am/HostingRecord.java index 6bb5def26b9d..bbf586123e1c 100644 --- a/services/core/java/com/android/server/am/HostingRecord.java +++ b/services/core/java/com/android/server/am/HostingRecord.java @@ -56,19 +56,27 @@ public final class HostingRecord { private final String mDefiningPackageName; private final int mDefiningUid; private final boolean mIsTopApp; + private final String mDefiningProcessName; public HostingRecord(String hostingType) { this(hostingType, null /* hostingName */, REGULAR_ZYGOTE, null /* definingPackageName */, - -1 /* mDefiningUid */, false /* isTopApp */); + -1 /* mDefiningUid */, false /* isTopApp */, null /* definingProcessName */); } public HostingRecord(String hostingType, ComponentName hostingName) { this(hostingType, hostingName, REGULAR_ZYGOTE); } + public HostingRecord(String hostingType, ComponentName hostingName, String definingPackageName, + int definingUid, String definingProcessName) { + this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE, definingPackageName, + definingUid, false /* isTopApp */, definingProcessName); + } + public HostingRecord(String hostingType, ComponentName hostingName, boolean isTopApp) { this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE, - null /* definingPackageName */, -1 /* mDefiningUid */, isTopApp /* isTopApp */); + null /* definingPackageName */, -1 /* mDefiningUid */, isTopApp /* isTopApp */, + null /* definingProcessName */); } public HostingRecord(String hostingType, String hostingName) { @@ -81,17 +89,19 @@ public final class HostingRecord { private HostingRecord(String hostingType, String hostingName, int hostingZygote) { this(hostingType, hostingName, hostingZygote, null /* definingPackageName */, - -1 /* mDefiningUid */, false /* isTopApp */); + -1 /* mDefiningUid */, false /* isTopApp */, null /* definingProcessName */); } private HostingRecord(String hostingType, String hostingName, int hostingZygote, - String definingPackageName, int definingUid, boolean isTopApp) { + String definingPackageName, int definingUid, boolean isTopApp, + String definingProcessName) { mHostingType = hostingType; mHostingName = hostingName; mHostingZygote = hostingZygote; mDefiningPackageName = definingPackageName; mDefiningUid = definingUid; mIsTopApp = isTopApp; + mDefiningProcessName = definingProcessName; } public String getType() { @@ -127,12 +137,24 @@ public final class HostingRecord { } /** + * Returns the processName of the component we want to start as specified in the defining app's + * manifest. + * + * @return the processName of the process in the hosting application + */ + public String getDefiningProcessName() { + return mDefiningProcessName; + } + + /** * Creates a HostingRecord for a process that must spawn from the webview zygote * @param hostingName name of the component to be hosted in this process * @return The constructed HostingRecord */ - public static HostingRecord byWebviewZygote(ComponentName hostingName) { - return new HostingRecord("", hostingName.toShortString(), WEBVIEW_ZYGOTE); + public static HostingRecord byWebviewZygote(ComponentName hostingName, + String definingPackageName, int definingUid, String definingProcessName) { + return new HostingRecord("", hostingName.toShortString(), WEBVIEW_ZYGOTE, + definingPackageName, definingUid, false /* isTopApp */, definingProcessName); } /** @@ -143,9 +165,9 @@ public final class HostingRecord { * @return The constructed HostingRecord */ public static HostingRecord byAppZygote(ComponentName hostingName, String definingPackageName, - int definingUid) { + int definingUid, String definingProcessName) { return new HostingRecord("", hostingName.toShortString(), APP_ZYGOTE, - definingPackageName, definingUid, false /* isTopApp */); + definingPackageName, definingUid, false /* isTopApp */, definingProcessName); } /** diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 41cd61da022a..2c2e7c40c9c3 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -70,7 +70,6 @@ import android.app.IApplicationThread; import android.app.IProcessObserver; import android.app.IUidObserver; import android.compat.annotation.ChangeId; -import android.compat.annotation.Disabled; import android.compat.annotation.EnabledAfter; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -363,59 +362,6 @@ public final class ProcessList { private static final long LMKD_RECONNECT_DELAY_MS = 1000; /** - * Native heap allocations will now have a non-zero tag in the most significant byte. - * @see <a href="https://source.android.com/devices/tech/debug/tagged-pointers">Tagged - * Pointers</a> - */ - @ChangeId - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q) - private static final long NATIVE_HEAP_POINTER_TAGGING = 135754954; // This is a bug id. - - /** - * Native heap allocations in AppZygote process and its descendants will now have a - * non-zero tag in the most significant byte. - * @see <a href="https://source.android.com/devices/tech/debug/tagged-pointers">Tagged - * Pointers</a> - */ - @ChangeId - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S) - private static final long NATIVE_HEAP_POINTER_TAGGING_APP_ZYGOTE = 207557677; - - /** - * Enable asynchronous (ASYNC) memory tag checking in this process. This - * flag will only have an effect on hardware supporting the ARM Memory - * Tagging Extension (MTE). - */ - @ChangeId - @Disabled - private static final long NATIVE_MEMTAG_ASYNC = 135772972; // This is a bug id. - - /** - * Enable synchronous (SYNC) memory tag checking in this process. This flag - * will only have an effect on hardware supporting the ARM Memory Tagging - * Extension (MTE). If both NATIVE_MEMTAG_ASYNC and this option is selected, - * this option takes preference and MTE is enabled in SYNC mode. - */ - @ChangeId - @Disabled - private static final long NATIVE_MEMTAG_SYNC = 177438394; // This is a bug id. - - /** - * Enable automatic zero-initialization of native heap memory allocations. - */ - @ChangeId - @Disabled - private static final long NATIVE_HEAP_ZERO_INIT = 178038272; // This is a bug id. - - /** - * Enable sampled memory bug detection in the app. - * @see <a href="https://source.android.com/devices/tech/debug/gwp-asan">GWP-ASan</a>. - */ - @ChangeId - @Disabled - private static final long GWP_ASAN = 135634846; // This is a bug id. - - /** * Apps have no access to the private data directories of any other app, even if the other * app has made them world-readable. */ @@ -1681,136 +1627,6 @@ public final class ProcessList { return gidArray; } - private int memtagModeToZygoteMemtagLevel(int memtagMode) { - switch (memtagMode) { - case ApplicationInfo.MEMTAG_ASYNC: - return Zygote.MEMORY_TAG_LEVEL_ASYNC; - case ApplicationInfo.MEMTAG_SYNC: - return Zygote.MEMORY_TAG_LEVEL_SYNC; - default: - return Zygote.MEMORY_TAG_LEVEL_NONE; - } - } - - // Returns the requested memory tagging level. - private int getRequestedMemtagLevel(ProcessRecord app) { - // Look at the process attribute first. - if (app.processInfo != null - && app.processInfo.memtagMode != ApplicationInfo.MEMTAG_DEFAULT) { - return memtagModeToZygoteMemtagLevel(app.processInfo.memtagMode); - } - - // Then at the application attribute. - if (app.info.getMemtagMode() != ApplicationInfo.MEMTAG_DEFAULT) { - return memtagModeToZygoteMemtagLevel(app.info.getMemtagMode()); - } - - if (mPlatformCompat.isChangeEnabled(NATIVE_MEMTAG_SYNC, app.info)) { - return Zygote.MEMORY_TAG_LEVEL_SYNC; - } - - if (mPlatformCompat.isChangeEnabled(NATIVE_MEMTAG_ASYNC, app.info)) { - return Zygote.MEMORY_TAG_LEVEL_ASYNC; - } - - // Check to ensure the app hasn't explicitly opted-out of TBI via. the manifest attribute. - if (!app.info.allowsNativeHeapPointerTagging()) { - return Zygote.MEMORY_TAG_LEVEL_NONE; - } - - String defaultLevel = SystemProperties.get("persist.arm64.memtag.app_default"); - if ("sync".equals(defaultLevel)) { - return Zygote.MEMORY_TAG_LEVEL_SYNC; - } else if ("async".equals(defaultLevel)) { - return Zygote.MEMORY_TAG_LEVEL_ASYNC; - } - - // Check to see that the compat feature for TBI is enabled. - if (mPlatformCompat.isChangeEnabled(NATIVE_HEAP_POINTER_TAGGING, app.info)) { - return Zygote.MEMORY_TAG_LEVEL_TBI; - } - - return Zygote.MEMORY_TAG_LEVEL_NONE; - } - - private int decideTaggingLevel(ProcessRecord app) { - // Get the desired tagging level (app manifest + compat features). - int level = getRequestedMemtagLevel(app); - - // Take into account the hardware capabilities. - if (Zygote.nativeSupportsMemoryTagging()) { - // MTE devices can not do TBI, because the Zygote process already has live MTE - // allocations. Downgrade TBI to NONE. - if (level == Zygote.MEMORY_TAG_LEVEL_TBI) { - level = Zygote.MEMORY_TAG_LEVEL_NONE; - } - } else if (Zygote.nativeSupportsTaggedPointers()) { - // TBI-but-not-MTE devices downgrade MTE modes to TBI. - // The idea is that if an app opts into full hardware tagging (MTE), it must be ok with - // the "fake" pointer tagging (TBI). - if (level == Zygote.MEMORY_TAG_LEVEL_ASYNC || level == Zygote.MEMORY_TAG_LEVEL_SYNC) { - level = Zygote.MEMORY_TAG_LEVEL_TBI; - } - } else { - // Otherwise disable all tagging. - level = Zygote.MEMORY_TAG_LEVEL_NONE; - } - - return level; - } - - private int decideTaggingLevelForAppZygote(ProcessRecord app) { - int level = decideTaggingLevel(app); - // TBI ("fake" pointer tagging) in AppZygote is controlled by a separate compat feature. - if (!mPlatformCompat.isChangeEnabled(NATIVE_HEAP_POINTER_TAGGING_APP_ZYGOTE, app.info) - && level == Zygote.MEMORY_TAG_LEVEL_TBI) { - level = Zygote.MEMORY_TAG_LEVEL_NONE; - } - return level; - } - - private int decideGwpAsanLevel(ProcessRecord app) { - // Look at the process attribute first. - if (app.processInfo != null - && app.processInfo.gwpAsanMode != ApplicationInfo.GWP_ASAN_DEFAULT) { - return app.processInfo.gwpAsanMode == ApplicationInfo.GWP_ASAN_ALWAYS - ? Zygote.GWP_ASAN_LEVEL_ALWAYS - : Zygote.GWP_ASAN_LEVEL_NEVER; - } - // Then at the application attribute. - if (app.info.getGwpAsanMode() != ApplicationInfo.GWP_ASAN_DEFAULT) { - return app.info.getGwpAsanMode() == ApplicationInfo.GWP_ASAN_ALWAYS - ? Zygote.GWP_ASAN_LEVEL_ALWAYS - : Zygote.GWP_ASAN_LEVEL_NEVER; - } - // If the app does not specify gwpAsanMode, the default behavior is lottery among the - // system apps, and disabled for user apps, unless overwritten by the compat feature. - if (mPlatformCompat.isChangeEnabled(GWP_ASAN, app.info)) { - return Zygote.GWP_ASAN_LEVEL_ALWAYS; - } - if ((app.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - return Zygote.GWP_ASAN_LEVEL_LOTTERY; - } - return Zygote.GWP_ASAN_LEVEL_NEVER; - } - - private boolean enableNativeHeapZeroInit(ProcessRecord app) { - // Look at the process attribute first. - if (app.processInfo != null - && app.processInfo.nativeHeapZeroInitialized != ApplicationInfo.ZEROINIT_DEFAULT) { - return app.processInfo.nativeHeapZeroInitialized == ApplicationInfo.ZEROINIT_ENABLED; - } - // Then at the application attribute. - if (app.info.getNativeHeapZeroInitialized() != ApplicationInfo.ZEROINIT_DEFAULT) { - return app.info.getNativeHeapZeroInitialized() == ApplicationInfo.ZEROINIT_ENABLED; - } - // Compat feature last. - if (mPlatformCompat.isChangeEnabled(NATIVE_HEAP_ZERO_INIT, app.info)) { - return true; - } - return false; - } - /** * @return {@code true} if process start is successful, false otherwise. */ @@ -1992,8 +1808,6 @@ public final class ProcessList { runtimeFlags |= Zygote.USE_APP_IMAGE_STARTUP_CACHE; } - runtimeFlags |= decideGwpAsanLevel(app); - String invokeWith = null; if ((app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { // Debuggable apps may include a wrapper script with their library directory. @@ -2024,23 +1838,21 @@ public final class ProcessList { app.setRequiredAbi(requiredAbi); app.setInstructionSet(instructionSet); - // If instructionSet is non-null, this indicates that the system_server is spawning a - // process with an ISA that may be different from its own. System (kernel and hardware) - // compatibility for these features is checked in the decideTaggingLevel in the - // system_server process (not the child process). As both MTE and TBI are only supported - // in aarch64, we can simply ensure that the new process is also aarch64. This prevents - // the mismatch where a 64-bit system server spawns a 32-bit child that thinks it should - // enable some tagging variant. Theoretically, a 32-bit system server could exist that - // spawns 64-bit processes, in which case the new process won't get any tagging. This is - // fine as we haven't seen this configuration in practice, and we can reasonable assume - // that if tagging is desired, the system server will be 64-bit. - if (instructionSet == null || instructionSet.equals("arm64")) { - runtimeFlags |= decideTaggingLevel(app); + // If this was an external service, the package name and uid in the passed in + // ApplicationInfo have been changed to match those of the calling package; + // that will incorrectly apply compat feature overrides for the calling package instead + // of the defining one. + ApplicationInfo definingAppInfo; + if (hostingRecord.getDefiningPackageName() != null) { + definingAppInfo = new ApplicationInfo(app.info); + definingAppInfo.packageName = hostingRecord.getDefiningPackageName(); + definingAppInfo.uid = uid; + } else { + definingAppInfo = app.info; } - if (enableNativeHeapZeroInit(app)) { - runtimeFlags |= Zygote.NATIVE_HEAP_ZERO_INIT; - } + runtimeFlags |= Zygote.getMemorySafetyRuntimeFlags( + definingAppInfo, app.processInfo, instructionSet, mPlatformCompat); // the per-user SELinux context must be set if (TextUtils.isEmpty(app.info.seInfoUser)) { @@ -2299,8 +2111,7 @@ public final class ProcessList { // not the calling one. appInfo.packageName = app.getHostingRecord().getDefiningPackageName(); appInfo.uid = uid; - int runtimeFlags = decideTaggingLevelForAppZygote(app); - appZygote = new AppZygote(appInfo, uid, firstUid, lastUid, runtimeFlags); + appZygote = new AppZygote(appInfo, app.processInfo, uid, firstUid, lastUid); mAppZygotes.put(app.info.processName, uid, appZygote); zygoteProcessList = new ArrayList<ProcessRecord>(); mAppZygoteProcesses.put(appZygote, zygoteProcessList); @@ -3158,7 +2969,8 @@ public final class ProcessList { FrameworkStatsLog.write(FrameworkStatsLog.ISOLATED_UID_CHANGED, info.uid, uid, FrameworkStatsLog.ISOLATED_UID_CHANGED__EVENT__CREATED); } - final ProcessRecord r = new ProcessRecord(mService, info, proc, uid); + final ProcessRecord r = new ProcessRecord(mService, info, proc, uid, + hostingRecord.getDefiningUid(), hostingRecord.getDefiningProcessName()); final ProcessStateRecord state = r.mState; if (!mService.mBooted && !mService.mBooting diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index be187e21db47..7672d10e27bd 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -492,24 +492,33 @@ class ProcessRecord implements WindowProcessListener { ProcessRecord(ActivityManagerService _service, ApplicationInfo _info, String _processName, int _uid) { + this(_service, _info, _processName, _uid, -1, null); + } + + ProcessRecord(ActivityManagerService _service, ApplicationInfo _info, String _processName, + int _uid, int _definingUid, String _definingProcessName) { mService = _service; mProcLock = _service.mProcLock; info = _info; ProcessInfo procInfo = null; if (_service.mPackageManagerInt != null) { - ArrayMap<String, ProcessInfo> processes = - _service.mPackageManagerInt.getProcessesForUid(_uid); - if (processes != null) { - procInfo = processes.get(_processName); - if (procInfo != null && procInfo.deniedPermissions == null - && procInfo.gwpAsanMode == ApplicationInfo.GWP_ASAN_DEFAULT - && procInfo.memtagMode == ApplicationInfo.MEMTAG_DEFAULT - && procInfo.nativeHeapZeroInitialized == ApplicationInfo.ZEROINIT_DEFAULT) { - // If this process hasn't asked for permissions to be denied, or for a - // non-default GwpAsan mode, or any other non-default setting, then we don't - // care about it. - procInfo = null; - } + if (_definingUid > 0) { + ArrayMap<String, ProcessInfo> processes = + _service.mPackageManagerInt.getProcessesForUid(_definingUid); + if (processes != null) procInfo = processes.get(_definingProcessName); + } else { + ArrayMap<String, ProcessInfo> processes = + _service.mPackageManagerInt.getProcessesForUid(_uid); + if (processes != null) procInfo = processes.get(_processName); + } + if (procInfo != null && procInfo.deniedPermissions == null + && procInfo.gwpAsanMode == ApplicationInfo.GWP_ASAN_DEFAULT + && procInfo.memtagMode == ApplicationInfo.MEMTAG_DEFAULT + && procInfo.nativeHeapZeroInitialized == ApplicationInfo.ZEROINIT_DEFAULT) { + // If this process hasn't asked for permissions to be denied, or for a + // non-default GwpAsan mode, or any other non-default setting, then we don't + // care about it. + procInfo = null; } } processInfo = procInfo; diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index 0edbea0dbd28..3efd8ad0581b 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -1320,15 +1320,26 @@ public final class GameManagerService extends IGameManagerService.Stub { // Make sure after resetting the game mode is still supported. // If not, set the game mode to standard int gameMode = getGameMode(packageName, userId); - int newGameMode = gameMode; GamePackageConfiguration config = null; synchronized (mOverrideConfigLock) { config = mOverrideConfigs.get(packageName); } - synchronized (mDeviceConfigLock) { - config = mConfigs.get(packageName); + if (config == null) { + synchronized (mDeviceConfigLock) { + config = mConfigs.get(packageName); + } } + final int newGameMode = getNewGameMode(gameMode, config); + if (gameMode != newGameMode) { + setGameMode(packageName, GameManager.GAME_MODE_STANDARD, userId); + return; + } + setGameMode(packageName, gameMode, userId); + } + + private int getNewGameMode(int gameMode, GamePackageConfiguration config) { + int newGameMode = gameMode; if (config != null) { int modesBitfield = config.getAvailableGameModesBitfield(); // Remove UNSUPPORTED to simplify the logic here, since we really just @@ -1350,11 +1361,7 @@ public final class GameManagerService extends IGameManagerService.Stub { // UNSUPPORTED, then set to UNSUPPORTED newGameMode = GameManager.GAME_MODE_UNSUPPORTED; } - if (gameMode != newGameMode) { - setGameMode(packageName, GameManager.GAME_MODE_STANDARD, userId); - return; - } - setGameMode(packageName, gameMode, userId); + return newGameMode; } /** @@ -1412,7 +1419,6 @@ public final class GameManagerService extends IGameManagerService.Stub { } for (final String packageName : packageNames) { int gameMode = getGameMode(packageName, userId); - int newGameMode = gameMode; // Make sure the user settings and package configs don't conflict. // I.e. the user setting is set to a mode that no longer available due to // config/manifest changes. @@ -1421,27 +1427,7 @@ public final class GameManagerService extends IGameManagerService.Stub { synchronized (mDeviceConfigLock) { config = mConfigs.get(packageName); } - if (config != null) { - int modesBitfield = config.getAvailableGameModesBitfield(); - // Remove UNSUPPORTED to simplify the logic here, since we really just - // want to check if we support selectable game modes - modesBitfield &= ~modeToBitmask(GameManager.GAME_MODE_UNSUPPORTED); - if (!bitFieldContainsModeBitmask(modesBitfield, gameMode)) { - if (bitFieldContainsModeBitmask(modesBitfield, - GameManager.GAME_MODE_STANDARD)) { - // If the current set mode isn't supported, - // but we support STANDARD, then set the mode to STANDARD. - newGameMode = GameManager.GAME_MODE_STANDARD; - } else { - // If we don't support any game modes, then set to UNSUPPORTED - newGameMode = GameManager.GAME_MODE_UNSUPPORTED; - } - } - } else if (gameMode != GameManager.GAME_MODE_UNSUPPORTED) { - // If we have no config for the package, but the configured mode is not - // UNSUPPORTED, then set to UNSUPPORTED - newGameMode = GameManager.GAME_MODE_UNSUPPORTED; - } + final int newGameMode = getNewGameMode(gameMode, config); if (newGameMode != gameMode) { setGameMode(packageName, newGameMode, userId); } diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index 79705a32c264..bf69284df2f6 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -20,12 +20,8 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE; import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_DEFAULT; -import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE; -import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_FACE_SCANNING; -import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_FP_SCANNING; -import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_SWITCHING; -import static com.android.server.biometrics.BiometricServiceStateProto.MULTI_SENSOR_STATE_UNKNOWN; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE; @@ -100,14 +96,6 @@ public final class AuthSession implements IBinder.DeathRecipient { @Retention(RetentionPolicy.SOURCE) @interface SessionState {} - /** Defined in biometrics.proto */ - @IntDef({ - MULTI_SENSOR_STATE_UNKNOWN, - MULTI_SENSOR_STATE_FACE_SCANNING, - MULTI_SENSOR_STATE_FP_SCANNING}) - @Retention(RetentionPolicy.SOURCE) - @interface MultiSensorState {} - /** * Notify the holder of the AuthSession that the caller/client's binder has died. The * holder (BiometricService) should schedule {@link AuthSession#onClientDied()} to be run @@ -119,7 +107,7 @@ public final class AuthSession implements IBinder.DeathRecipient { private final Context mContext; private final IStatusBarService mStatusBarService; - private final IBiometricSysuiReceiver mSysuiReceiver; + @VisibleForTesting final IBiometricSysuiReceiver mSysuiReceiver; private final KeyStore mKeyStore; private final Random mRandom; private final ClientDeathReceiver mClientDeathReceiver; @@ -133,7 +121,7 @@ public final class AuthSession implements IBinder.DeathRecipient { private final long mRequestId; private final long mOperationId; private final int mUserId; - private final IBiometricSensorReceiver mSensorReceiver; + @VisibleForTesting final IBiometricSensorReceiver mSensorReceiver; // Original receiver from BiometricPrompt. private final IBiometricServiceReceiver mClientReceiver; private final String mOpPackageName; @@ -143,10 +131,10 @@ public final class AuthSession implements IBinder.DeathRecipient { // The current state, which can be either idle, called, or started private @SessionState int mState = STATE_AUTH_IDLE; private @BiometricMultiSensorMode int mMultiSensorMode; - private @MultiSensorState int mMultiSensorState; private int[] mSensors; // TODO(b/197265902): merge into state private boolean mCancelled; + private int mAuthenticatedSensorId = -1; // For explicit confirmation, do not send to keystore until the user has confirmed // the authentication. private byte[] mTokenEscrow; @@ -232,8 +220,16 @@ public final class AuthSession implements IBinder.DeathRecipient { } } - private void setSensorsToStateWaitingForCookie() throws RemoteException { + private void setSensorsToStateWaitingForCookie(boolean isTryAgain) throws RemoteException { for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { + @BiometricSensor.SensorState final int state = sensor.getSensorState(); + if (isTryAgain + && state != BiometricSensor.STATE_STOPPED + && state != BiometricSensor.STATE_CANCELING) { + Slog.d(TAG, "Skip retry because sensor: " + sensor.id + " is: " + state); + continue; + } + final int cookie = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1; final boolean requireConfirmation = isConfirmationRequired(sensor); @@ -254,7 +250,6 @@ public final class AuthSession implements IBinder.DeathRecipient { mState = STATE_SHOWING_DEVICE_CREDENTIAL; mSensors = new int[0]; mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT; - mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN; mStatusBarService.showAuthenticationDialog( mPromptInfo, @@ -269,7 +264,7 @@ public final class AuthSession implements IBinder.DeathRecipient { mMultiSensorMode); } else if (!mPreAuthInfo.eligibleSensors.isEmpty()) { // Some combination of biometric or biometric|credential is requested - setSensorsToStateWaitingForCookie(); + setSensorsToStateWaitingForCookie(false /* isTryAgain */); mState = STATE_AUTH_CALLED; } else { // No authenticators requested. This should never happen - an exception should have @@ -283,6 +278,10 @@ public final class AuthSession implements IBinder.DeathRecipient { Slog.w(TAG, "Received cookie but already cancelled (ignoring): " + cookie); return; } + if (hasAuthenticated()) { + Slog.d(TAG, "onCookieReceived after successful auth"); + return; + } for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { sensor.goToStateCookieReturnedIfCookieMatches(cookie); @@ -307,7 +306,6 @@ public final class AuthSession implements IBinder.DeathRecipient { } mMultiSensorMode = getMultiSensorModeForNewSession( mPreAuthInfo.eligibleSensors); - mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN; mStatusBarService.showAuthenticationDialog(mPromptInfo, mSysuiReceiver, @@ -381,9 +379,8 @@ public final class AuthSession implements IBinder.DeathRecipient { // sending the final error callback to the application. for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { try { - final boolean shouldCancel = filter.apply(sensor); - Slog.d(TAG, "sensorId: " + sensor.id + ", shouldCancel: " + shouldCancel); - if (shouldCancel) { + if (filter.apply(sensor)) { + Slog.d(TAG, "Cancelling sensorId: " + sensor.id); sensor.goToStateCancelling(mToken, mOpPackageName, mRequestId); } } catch (RemoteException e) { @@ -412,10 +409,16 @@ public final class AuthSession implements IBinder.DeathRecipient { } } + // do not propagate the error and let onAuthenticationSucceeded handle the new state + if (hasAuthenticated()) { + Slog.d(TAG, "onErrorReceived after successful auth (ignoring)"); + return false; + } + mErrorEscrow = error; mVendorCodeEscrow = vendorCode; - final @BiometricAuthenticator.Modality int modality = sensorIdToModality(sensorId); + @Modality final int modality = sensorIdToModality(sensorId); switch (mState) { case STATE_AUTH_CALLED: { @@ -430,7 +433,6 @@ public final class AuthSession implements IBinder.DeathRecipient { mState = STATE_SHOWING_DEVICE_CREDENTIAL; mMultiSensorMode = BIOMETRIC_MULTI_SENSOR_DEFAULT; - mMultiSensorState = MULTI_SENSOR_STATE_UNKNOWN; mSensors = new int[0]; mStatusBarService.showAuthenticationDialog( @@ -468,12 +470,6 @@ public final class AuthSession implements IBinder.DeathRecipient { return true; } else { mState = STATE_ERROR_PENDING_SYSUI; - if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT - && mMultiSensorState == MULTI_SENSOR_STATE_FACE_SCANNING) { - // wait for the UI to signal when modality should switch - Slog.d(TAG, "onErrorReceived: waiting for modality switch callback"); - mMultiSensorState = MULTI_SENSOR_STATE_SWITCHING; - } mStatusBarService.onBiometricError(modality, error, vendorCode); } break; @@ -505,6 +501,11 @@ public final class AuthSession implements IBinder.DeathRecipient { } void onAcquired(int sensorId, int acquiredInfo, int vendorCode) { + if (hasAuthenticated()) { + Slog.d(TAG, "onAcquired after successful auth"); + return; + } + final String message = getAcquiredMessageForSensor(sensorId, acquiredInfo, vendorCode); Slog.d(TAG, "sensorId: " + sensorId + " acquiredInfo: " + acquiredInfo + " message: " + message); @@ -520,6 +521,10 @@ public final class AuthSession implements IBinder.DeathRecipient { } void onSystemEvent(int event) { + if (hasAuthenticated()) { + Slog.d(TAG, "onSystemEvent after successful auth"); + return; + } if (!mPromptInfo.isReceiveSystemEvents()) { return; } @@ -538,53 +543,35 @@ public final class AuthSession implements IBinder.DeathRecipient { mState = STATE_AUTH_STARTED_UI_SHOWING; - if (mMultiSensorMode == BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) { - mMultiSensorState = MULTI_SENSOR_STATE_FACE_SCANNING; - } else { - startFingerprintSensorsNow(); - } - } - - // call 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 (mMultiSensorMode != BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT) { - Slog.e(TAG, "onStartFingerprint, unexpected mode: " + mMultiSensorMode); - return; - } - - 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); - } - - mMultiSensorState = MULTI_SENSOR_STATE_FP_SCANNING; - startFingerprintSensorsNow(); - } - - // unguarded helper for the above methods only - private void startFingerprintSensorsNow() { startAllPreparedFingerprintSensors(); mState = STATE_AUTH_STARTED_UI_SHOWING; } void onTryAgainPressed() { + if (hasAuthenticated()) { + Slog.d(TAG, "onTryAgainPressed after successful auth"); + return; + } + if (mState != STATE_AUTH_PAUSED) { Slog.w(TAG, "onTryAgainPressed, state: " + mState); } try { - setSensorsToStateWaitingForCookie(); + setSensorsToStateWaitingForCookie(true /* isTryAgain */); mState = STATE_AUTH_PAUSED_RESUMING; } catch (RemoteException e) { Slog.e(TAG, "RemoteException: " + e); } } - void onAuthenticationSucceeded(int sensorId, boolean strong, - byte[] token) { + void onAuthenticationSucceeded(int sensorId, boolean strong, byte[] token) { + if (hasAuthenticated()) { + Slog.d(TAG, "onAuthenticationSucceeded after successful auth"); + return; + } + + mAuthenticatedSensorId = sensorId; if (strong) { mTokenEscrow = token; } else { @@ -596,7 +583,7 @@ public final class AuthSession implements IBinder.DeathRecipient { try { // Notify SysUI that the biometric has been authenticated. SysUI already knows // the implicit/explicit state and will react accordingly. - mStatusBarService.onBiometricAuthenticated(); + mStatusBarService.onBiometricAuthenticated(sensorIdToModality(sensorId)); final boolean requireConfirmation = isConfirmationRequiredByAnyEligibleSensor(); @@ -609,20 +596,22 @@ public final class AuthSession implements IBinder.DeathRecipient { } catch (RemoteException e) { Slog.e(TAG, "RemoteException", e); } + + cancelAllSensors(sensor -> sensor.id != sensorId); } - void onAuthenticationRejected() { + void onAuthenticationRejected(int sensorId) { + if (hasAuthenticated()) { + Slog.d(TAG, "onAuthenticationRejected after successful auth"); + return; + } + try { - mStatusBarService.onBiometricError(TYPE_NONE, + mStatusBarService.onBiometricError(sensorIdToModality(sensorId), BiometricConstants.BIOMETRIC_PAUSED_REJECTED, 0 /* vendorCode */); - - // TODO: This logic will need to be updated if BP is multi-modal - if (hasPausableBiometric()) { - // Pause authentication. onBiometricAuthenticated(false) causes the - // dialog to show a "try again" button for passive modalities. + if (pauseSensorIfSupported(sensorId)) { mState = STATE_AUTH_PAUSED; } - mClientReceiver.onAuthenticationFailed(); } catch (RemoteException e) { Slog.e(TAG, "RemoteException", e); @@ -630,15 +619,34 @@ public final class AuthSession implements IBinder.DeathRecipient { } void onAuthenticationTimedOut(int sensorId, int cookie, int error, int vendorCode) { + if (hasAuthenticated()) { + Slog.d(TAG, "onAuthenticationTimedOut after successful auth"); + return; + } + try { mStatusBarService.onBiometricError(sensorIdToModality(sensorId), error, vendorCode); + pauseSensorIfSupported(sensorId); mState = STATE_AUTH_PAUSED; } catch (RemoteException e) { Slog.e(TAG, "RemoteException", e); } } + private boolean pauseSensorIfSupported(int sensorId) { + if (sensorIdToModality(sensorId) == TYPE_FACE) { + cancelAllSensors(sensor -> sensor.id == sensorId); + return true; + } + return false; + } + void onDeviceCredentialPressed() { + if (hasAuthenticated()) { + Slog.d(TAG, "onDeviceCredentialPressed after successful auth"); + return; + } + // Cancel authentication. Skip the token/package check since we are cancelling // from system server. The interface is permission protected so this is fine. cancelAllSensors(); @@ -666,6 +674,10 @@ public final class AuthSession implements IBinder.DeathRecipient { } } + private boolean hasAuthenticated() { + return mAuthenticatedSensorId != -1; + } + private void logOnDialogDismissed(@BiometricPrompt.DismissedReason int reason) { if (reason == BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED) { // Explicit auth, authentication confirmed. @@ -794,6 +806,11 @@ public final class AuthSession implements IBinder.DeathRecipient { * @return true if this AuthSession is finished, e.g. should be set to null */ boolean onCancelAuthSession(boolean force) { + if (hasAuthenticated()) { + Slog.d(TAG, "onCancelAuthSession after successful auth"); + return true; + } + mCancelled = true; final boolean authStarted = mState == STATE_AUTH_CALLED @@ -848,15 +865,6 @@ public final class AuthSession implements IBinder.DeathRecipient { return remainingCookies == 0; } - private boolean hasPausableBiometric() { - for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { - if (sensor.modality == TYPE_FACE) { - return true; - } - } - return false; - } - @SessionState int getState() { return mState; } @@ -919,7 +927,7 @@ public final class AuthSession implements IBinder.DeathRecipient { } if (hasFace && hasFingerprint) { - return BIOMETRIC_MULTI_SENSOR_FACE_THEN_FINGERPRINT; + return BIOMETRIC_MULTI_SENSOR_FINGERPRINT_AND_FACE; } return BIOMETRIC_MULTI_SENSOR_DEFAULT; } diff --git a/services/core/java/com/android/server/biometrics/BiometricSensor.java b/services/core/java/com/android/server/biometrics/BiometricSensor.java index 0333c3e247c0..7166783f0b23 100644 --- a/services/core/java/com/android/server/biometrics/BiometricSensor.java +++ b/services/core/java/com/android/server/biometrics/BiometricSensor.java @@ -131,8 +131,10 @@ public abstract class BiometricSensor { void goToStateCancelling(IBinder token, String opPackageName, long requestId) throws RemoteException { - impl.cancelAuthenticationFromService(token, opPackageName, requestId); - mSensorState = STATE_CANCELING; + if (mSensorState != STATE_CANCELING) { + impl.cancelAuthenticationFromService(token, opPackageName, requestId); + mSensorState = STATE_CANCELING; + } } void goToStoppedStateIfCookieMatches(int cookie, int error) { diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 758cf7a7d430..0d9b75481ea9 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -55,7 +55,6 @@ import android.os.DeadObjectException; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -84,6 +83,7 @@ import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; /** * System service that arbitrates the modality for BiometricPrompt to use. @@ -92,22 +92,6 @@ public class BiometricService extends SystemService { static final String TAG = "BiometricService"; - private static final int MSG_ON_AUTHENTICATION_SUCCEEDED = 2; - private static final int MSG_ON_AUTHENTICATION_REJECTED = 3; - private static final int MSG_ON_ERROR = 4; - private static final int MSG_ON_ACQUIRED = 5; - private static final int MSG_ON_DISMISSED = 6; - private static final int MSG_ON_TRY_AGAIN_PRESSED = 7; - private static final int MSG_ON_READY_FOR_AUTHENTICATION = 8; - private static final int MSG_AUTHENTICATE = 9; - private static final int MSG_CANCEL_AUTHENTICATION = 10; - private static final int MSG_ON_AUTHENTICATION_TIMED_OUT = 11; - private static final int MSG_ON_DEVICE_CREDENTIAL_PRESSED = 12; - private static final int MSG_ON_SYSTEM_EVENT = 13; - private static final int MSG_CLIENT_DIED = 14; - private static final int MSG_ON_DIALOG_ANIMATED_IN = 15; - private static final int MSG_ON_START_FINGERPRINT_NOW = 16; - private final Injector mInjector; private final DevicePolicyManager mDevicePolicyManager; @VisibleForTesting @@ -116,7 +100,7 @@ public class BiometricService extends SystemService { final SettingObserver mSettingObserver; private final List<EnabledOnKeyguardCallback> mEnabledOnKeyguardCallbacks; private final Random mRandom = new Random(); - @NonNull private final AtomicLong mRequestCounter; + @NonNull private final Supplier<Long> mRequestCounter; @VisibleForTesting IStatusBarService mStatusBarService; @@ -128,133 +112,13 @@ public class BiometricService extends SystemService { // Get and cache the available biometric authenticators and their associated info. final ArrayList<BiometricSensor> mSensors = new ArrayList<>(); + @VisibleForTesting BiometricStrengthController mBiometricStrengthController; // The current authentication session, null if idle/done. @VisibleForTesting - AuthSession mCurrentAuthSession; - - @VisibleForTesting - final Handler mHandler = new Handler(Looper.getMainLooper()) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_ON_AUTHENTICATION_SUCCEEDED: { - SomeArgs args = (SomeArgs) msg.obj; - handleAuthenticationSucceeded( - args.argi1 /* sensorId */, - (byte[]) args.arg1 /* token */); - args.recycle(); - break; - } - - case MSG_ON_AUTHENTICATION_REJECTED: { - handleAuthenticationRejected(); - break; - } - - case MSG_ON_ERROR: { - SomeArgs args = (SomeArgs) msg.obj; - handleOnError( - args.argi1 /* sensorId */, - args.argi2 /* cookie */, - args.argi3 /* error */, - args.argi4 /* vendorCode */); - args.recycle(); - break; - } - - case MSG_ON_ACQUIRED: { - SomeArgs args = (SomeArgs) msg.obj; - handleOnAcquired( - args.argi1 /* sensorId */, - args.argi2 /* acquiredInfo */, - args.argi3 /* vendorCode */); - args.recycle(); - break; - } - - case MSG_ON_DISMISSED: { - handleOnDismissed(msg.arg1, (byte[]) msg.obj); - break; - } - - case MSG_ON_TRY_AGAIN_PRESSED: { - handleOnTryAgainPressed(); - break; - } - - case MSG_ON_READY_FOR_AUTHENTICATION: { - SomeArgs args = (SomeArgs) msg.obj; - handleOnReadyForAuthentication( - args.argi1 /* cookie */); - args.recycle(); - break; - } - - case MSG_AUTHENTICATE: { - SomeArgs args = (SomeArgs) msg.obj; - handleAuthenticate( - (IBinder) args.arg1 /* token */, - (long) args.arg6 /* requestId */, - (long) args.arg2 /* operationId */, - args.argi1 /* userid */, - (IBiometricServiceReceiver) args.arg3 /* receiver */, - (String) args.arg4 /* opPackageName */, - (PromptInfo) args.arg5 /* promptInfo */); - args.recycle(); - break; - } - - case MSG_CANCEL_AUTHENTICATION: { - SomeArgs args = (SomeArgs) msg.obj; - handleCancelAuthentication((long) args.arg3 /* requestId */); - args.recycle(); - break; - } - - case MSG_ON_AUTHENTICATION_TIMED_OUT: { - SomeArgs args = (SomeArgs) msg.obj; - handleAuthenticationTimedOut( - args.argi1 /* sensorId */, - args.argi2 /* cookie */, - args.argi3 /* error */, - args.argi4 /* vendorCode */); - args.recycle(); - break; - } - - case MSG_ON_DEVICE_CREDENTIAL_PRESSED: { - handleOnDeviceCredentialPressed(); - break; - } - - case MSG_ON_SYSTEM_EVENT: { - handleOnSystemEvent((int) msg.obj); - break; - } - - case MSG_CLIENT_DIED: { - handleClientDied(); - break; - } - - case MSG_ON_DIALOG_ANIMATED_IN: { - handleOnDialogAnimatedIn(); - break; - } - - case MSG_ON_START_FINGERPRINT_NOW: { - handleOnStartFingerprintNow(); - break; - } - - default: - Slog.e(TAG, "Unknown message: " + msg); - break; - } - } - }; + AuthSession mAuthSession; + private final Handler mHandler = new Handler(Looper.getMainLooper()); /** * Tracks authenticatorId invalidation. For more details, see @@ -552,93 +416,74 @@ public class BiometricService extends SystemService { } // Receives events from individual biometric sensors. - @VisibleForTesting - final IBiometricSensorReceiver mBiometricSensorReceiver = new IBiometricSensorReceiver.Stub() { - @Override - public void onAuthenticationSucceeded(int sensorId, byte[] token) { - SomeArgs args = SomeArgs.obtain(); - args.argi1 = sensorId; - args.arg1 = token; - mHandler.obtainMessage(MSG_ON_AUTHENTICATION_SUCCEEDED, args).sendToTarget(); - } - - @Override - public void onAuthenticationFailed(int sensorId) { - Slog.v(TAG, "onAuthenticationFailed"); - mHandler.obtainMessage(MSG_ON_AUTHENTICATION_REJECTED).sendToTarget(); - } + private IBiometricSensorReceiver createBiometricSensorReceiver(final long requestId) { + return new IBiometricSensorReceiver.Stub() { + @Override + public void onAuthenticationSucceeded(int sensorId, byte[] token) { + mHandler.post(() -> handleAuthenticationSucceeded(requestId, sensorId, token)); + } - @Override - public void onError(int sensorId, int cookie, @BiometricConstants.Errors int error, - int vendorCode) { - // Determine if error is hard or soft error. Certain errors (such as TIMEOUT) are - // soft errors and we should allow the user to try authenticating again instead of - // dismissing BiometricPrompt. - if (error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT) { - SomeArgs args = SomeArgs.obtain(); - args.argi1 = sensorId; - args.argi2 = cookie; - args.argi3 = error; - args.argi4 = vendorCode; - mHandler.obtainMessage(MSG_ON_AUTHENTICATION_TIMED_OUT, args).sendToTarget(); - } else { - SomeArgs args = SomeArgs.obtain(); - args.argi1 = sensorId; - args.argi2 = cookie; - args.argi3 = error; - args.argi4 = vendorCode; - mHandler.obtainMessage(MSG_ON_ERROR, args).sendToTarget(); + @Override + public void onAuthenticationFailed(int sensorId) { + Slog.v(TAG, "onAuthenticationFailed"); + mHandler.post(() -> handleAuthenticationRejected(requestId, sensorId)); } - } - @Override - public void onAcquired(int sensorId, int acquiredInfo, int vendorCode) { - SomeArgs args = SomeArgs.obtain(); - args.argi1 = sensorId; - args.argi2 = acquiredInfo; - args.argi3 = vendorCode; - mHandler.obtainMessage(MSG_ON_ACQUIRED, args).sendToTarget(); - } - }; + @Override + public void onError(int sensorId, int cookie, @BiometricConstants.Errors int error, + int vendorCode) { + // Determine if error is hard or soft error. Certain errors (such as TIMEOUT) are + // soft errors and we should allow the user to try authenticating again instead of + // dismissing BiometricPrompt. + if (error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT) { + mHandler.post(() -> handleAuthenticationTimedOut( + requestId, sensorId, cookie, error, vendorCode)); + } else { + mHandler.post(() -> handleOnError( + requestId, sensorId, cookie, error, vendorCode)); + } + } - final IBiometricSysuiReceiver mSysuiReceiver = new IBiometricSysuiReceiver.Stub() { - @Override - public void onDialogDismissed(@BiometricPrompt.DismissedReason int reason, - @Nullable byte[] credentialAttestation) { - mHandler.obtainMessage(MSG_ON_DISMISSED, - reason, - 0 /* arg2 */, - credentialAttestation /* obj */).sendToTarget(); - } + @Override + public void onAcquired(int sensorId, int acquiredInfo, int vendorCode) { + mHandler.post(() -> handleOnAcquired( + requestId, sensorId, acquiredInfo, vendorCode)); + } + }; + } - @Override - public void onTryAgainPressed() { - mHandler.sendEmptyMessage(MSG_ON_TRY_AGAIN_PRESSED); - } + private IBiometricSysuiReceiver createSysuiReceiver(final long requestId) { + return new IBiometricSysuiReceiver.Stub() { + @Override + public void onDialogDismissed(@BiometricPrompt.DismissedReason int reason, + @Nullable byte[] credentialAttestation) { + mHandler.post(() -> handleOnDismissed(requestId, reason, credentialAttestation)); + } - @Override - public void onDeviceCredentialPressed() { - mHandler.sendEmptyMessage(MSG_ON_DEVICE_CREDENTIAL_PRESSED); - } + @Override + public void onTryAgainPressed() { + mHandler.post(() -> handleOnTryAgainPressed(requestId)); + } - @Override - public void onSystemEvent(int event) { - mHandler.obtainMessage(MSG_ON_SYSTEM_EVENT, event).sendToTarget(); - } + @Override + public void onDeviceCredentialPressed() { + mHandler.post(() -> handleOnDeviceCredentialPressed(requestId)); + } - @Override - public void onDialogAnimatedIn() { - mHandler.obtainMessage(MSG_ON_DIALOG_ANIMATED_IN).sendToTarget(); - } + @Override + public void onSystemEvent(int event) { + mHandler.post(() -> handleOnSystemEvent(requestId, event)); + } - @Override - public void onStartFingerprintNow() { - mHandler.obtainMessage(MSG_ON_START_FINGERPRINT_NOW).sendToTarget(); - } - }; + @Override + public void onDialogAnimatedIn() { + mHandler.post(() -> handleOnDialogAnimatedIn(requestId)); + } + }; + } - private final AuthSession.ClientDeathReceiver mClientDeathReceiver = () -> { - mHandler.sendEmptyMessage(MSG_CLIENT_DIED); + private AuthSession.ClientDeathReceiver createClientDeathReceiver(final long requestId) { + return () -> mHandler.post(() -> handleClientDied(requestId)); }; /** @@ -679,12 +524,10 @@ public class BiometricService extends SystemService { } @Override // Binder call - public void onReadyForAuthentication(int cookie) { + public void onReadyForAuthentication(long requestId, int cookie) { checkInternalPermission(); - SomeArgs args = SomeArgs.obtain(); - args.argi1 = cookie; - mHandler.obtainMessage(MSG_ON_READY_FOR_AUTHENTICATION, args).sendToTarget(); + mHandler.post(() -> handleOnReadyForAuthentication(requestId, cookie)); } @Override // Binder call @@ -711,18 +554,9 @@ public class BiometricService extends SystemService { } } - final long requestId = mRequestCounter.incrementAndGet(); - - SomeArgs args = SomeArgs.obtain(); - args.arg1 = token; - args.arg2 = operationId; - args.argi1 = userId; - args.arg3 = receiver; - args.arg4 = opPackageName; - args.arg5 = promptInfo; - args.arg6 = requestId; - - mHandler.obtainMessage(MSG_AUTHENTICATE, args).sendToTarget(); + final long requestId = mRequestCounter.get(); + mHandler.post(() -> handleAuthenticate( + token, requestId, operationId, userId, receiver, opPackageName, promptInfo)); return requestId; } @@ -736,7 +570,7 @@ public class BiometricService extends SystemService { args.arg2 = opPackageName; args.arg3 = requestId; - mHandler.obtainMessage(MSG_CANCEL_AUTHENTICATION, args).sendToTarget(); + mHandler.post(() -> handleCancelAuthentication(requestId)); } @Override // Binder call @@ -1002,8 +836,7 @@ public class BiometricService extends SystemService { Slog.d(TAG, "ClearSchedulerBuffer: " + clearSchedulerBuffer); final ProtoOutputStream proto = new ProtoOutputStream(fd); proto.write(BiometricServiceStateProto.AUTH_SESSION_STATE, - mCurrentAuthSession != null ? mCurrentAuthSession.getState() - : STATE_AUTH_IDLE); + mAuthSession != null ? mAuthSession.getState() : STATE_AUTH_IDLE); for (BiometricSensor sensor : mSensors) { byte[] serviceState = sensor.impl .dumpSensorServiceStateProto(clearSchedulerBuffer); @@ -1128,8 +961,9 @@ public class BiometricService extends SystemService { CoexCoordinator.FACE_HAPTIC_DISABLE, 1) != 0; } - public AtomicLong getRequestGenerator() { - return new AtomicLong(0); + public Supplier<Long> getRequestGenerator() { + final AtomicLong generator = new AtomicLong(0); + return () -> generator.incrementAndGet(); } } @@ -1202,172 +1036,184 @@ public class BiometricService extends SystemService { return false; } - private void handleAuthenticationSucceeded(int sensorId, byte[] token) { + @Nullable + private AuthSession getAuthSessionIfCurrent(long requestId) { + final AuthSession session = mAuthSession; + if (session != null && session.getRequestId() == requestId) { + return session; + } + return null; + } + + private void handleAuthenticationSucceeded(long requestId, int sensorId, byte[] token) { Slog.v(TAG, "handleAuthenticationSucceeded(), sensorId: " + sensorId); // Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded // after user dismissed/canceled dialog). - if (mCurrentAuthSession == null) { + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { Slog.e(TAG, "handleAuthenticationSucceeded: AuthSession is null"); return; } - mCurrentAuthSession.onAuthenticationSucceeded(sensorId, isStrongBiometric(sensorId), token); + session.onAuthenticationSucceeded(sensorId, isStrongBiometric(sensorId), token); } - private void handleAuthenticationRejected() { + private void handleAuthenticationRejected(long requestId, int sensorId) { Slog.v(TAG, "handleAuthenticationRejected()"); // Should never happen, log this to catch bad HAL behavior (e.g. auth rejected // after user dismissed/canceled dialog). - if (mCurrentAuthSession == null) { - Slog.e(TAG, "handleAuthenticationRejected: AuthSession is null"); + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.w(TAG, "handleAuthenticationRejected: AuthSession is not current"); return; } - mCurrentAuthSession.onAuthenticationRejected(); + session.onAuthenticationRejected(sensorId); } - private void handleAuthenticationTimedOut(int sensorId, int cookie, int error, int vendorCode) { + private void handleAuthenticationTimedOut(long requestId, int sensorId, int cookie, int error, + int vendorCode) { Slog.v(TAG, "handleAuthenticationTimedOut(), sensorId: " + sensorId + ", cookie: " + cookie + ", error: " + error + ", vendorCode: " + vendorCode); // Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded // after user dismissed/canceled dialog). - if (mCurrentAuthSession == null) { - Slog.e(TAG, "handleAuthenticationTimedOut: AuthSession is null"); + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.w(TAG, "handleAuthenticationTimedOut: AuthSession is not current"); return; } - mCurrentAuthSession.onAuthenticationTimedOut(sensorId, cookie, error, vendorCode); + session.onAuthenticationTimedOut(sensorId, cookie, error, vendorCode); } - private void handleOnError(int sensorId, int cookie, @BiometricConstants.Errors int error, - int vendorCode) { + private void handleOnError(long requestId, int sensorId, int cookie, + @BiometricConstants.Errors int error, int vendorCode) { Slog.d(TAG, "handleOnError() sensorId: " + sensorId + ", cookie: " + cookie + ", error: " + error + ", vendorCode: " + vendorCode); - if (mCurrentAuthSession == null) { - Slog.e(TAG, "handleOnError: AuthSession is null"); + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.w(TAG, "handleOnError: AuthSession is not current"); return; } try { - final boolean finished = mCurrentAuthSession - .onErrorReceived(sensorId, cookie, error, vendorCode); + final boolean finished = session.onErrorReceived(sensorId, cookie, error, vendorCode); if (finished) { Slog.d(TAG, "handleOnError: AuthSession finished"); - mCurrentAuthSession = null; + mAuthSession = null; } } catch (RemoteException e) { Slog.e(TAG, "RemoteException", e); } } - private void handleOnAcquired(int sensorId, int acquiredInfo, int vendorCode) { + private void handleOnAcquired(long requestId, int sensorId, int acquiredInfo, int vendorCode) { // Should never happen, log this to catch bad HAL behavior (e.g. auth succeeded // after user dismissed/canceled dialog). - if (mCurrentAuthSession == null) { - Slog.e(TAG, "onAcquired: AuthSession is null"); + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.w(TAG, "onAcquired: AuthSession is not current"); return; } - mCurrentAuthSession.onAcquired(sensorId, acquiredInfo, vendorCode); + session.onAcquired(sensorId, acquiredInfo, vendorCode); } - private void handleOnDismissed(@BiometricPrompt.DismissedReason int reason, + private void handleOnDismissed(long requestId, @BiometricPrompt.DismissedReason int reason, @Nullable byte[] credentialAttestation) { - if (mCurrentAuthSession == null) { - Slog.e(TAG, "onDismissed: " + reason + ", AuthSession is null"); + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.e(TAG, "onDismissed: " + reason + ", AuthSession is not current"); return; } - mCurrentAuthSession.onDialogDismissed(reason, credentialAttestation); - mCurrentAuthSession = null; + session.onDialogDismissed(reason, credentialAttestation); + mAuthSession = null; } - private void handleOnTryAgainPressed() { + private void handleOnTryAgainPressed(long requestId) { Slog.d(TAG, "onTryAgainPressed"); // No need to check permission, since it can only be invoked by SystemUI // (or system server itself). - if (mCurrentAuthSession == null) { - Slog.e(TAG, "handleOnTryAgainPressed: AuthSession is null"); + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.w(TAG, "handleOnTryAgainPressed: AuthSession is not current"); return; } - mCurrentAuthSession.onTryAgainPressed(); + session.onTryAgainPressed(); } - private void handleOnDeviceCredentialPressed() { + private void handleOnDeviceCredentialPressed(long requestId) { Slog.d(TAG, "onDeviceCredentialPressed"); - if (mCurrentAuthSession == null) { - Slog.e(TAG, "handleOnDeviceCredentialPressed: AuthSession is null"); + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.w(TAG, "handleOnDeviceCredentialPressed: AuthSession is not current"); return; } - mCurrentAuthSession.onDeviceCredentialPressed(); + session.onDeviceCredentialPressed(); } - private void handleOnSystemEvent(int event) { + private void handleOnSystemEvent(long requestId, int event) { Slog.d(TAG, "onSystemEvent: " + event); - if (mCurrentAuthSession == null) { - Slog.e(TAG, "handleOnSystemEvent: AuthSession is null"); + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.w(TAG, "handleOnSystemEvent: AuthSession is not current"); return; } - mCurrentAuthSession.onSystemEvent(event); + session.onSystemEvent(event); } - private void handleClientDied() { - if (mCurrentAuthSession == null) { - Slog.e(TAG, "handleClientDied: AuthSession is null"); + private void handleClientDied(long requestId) { + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.w(TAG, "handleClientDied: AuthSession is not current"); return; } - Slog.e(TAG, "Session: " + mCurrentAuthSession); - final boolean finished = mCurrentAuthSession.onClientDied(); + Slog.e(TAG, "Session: " + session); + final boolean finished = session.onClientDied(); if (finished) { - mCurrentAuthSession = null; + mAuthSession = null; } } - private void handleOnDialogAnimatedIn() { + private void handleOnDialogAnimatedIn(long requestId) { Slog.d(TAG, "handleOnDialogAnimatedIn"); - if (mCurrentAuthSession == null) { - Slog.e(TAG, "handleOnDialogAnimatedIn: AuthSession is null"); - return; - } - - mCurrentAuthSession.onDialogAnimatedIn(); - } - private void handleOnStartFingerprintNow() { - Slog.d(TAG, "handleOnStartFingerprintNow"); - if (mCurrentAuthSession == null) { - Slog.e(TAG, "handleOnStartFingerprintNow: AuthSession is null"); + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.w(TAG, "handleOnDialogAnimatedIn: AuthSession is not current"); return; } - mCurrentAuthSession.onStartFingerprint(); + session.onDialogAnimatedIn(); } /** * Invoked when each service has notified that its client is ready to be started. When * all biometrics are ready, this invokes the SystemUI dialog through StatusBar. */ - private void handleOnReadyForAuthentication(int cookie) { - if (mCurrentAuthSession == null) { + private void handleOnReadyForAuthentication(long requestId, int cookie) { + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { // Only should happen if a biometric was locked out when authenticate() was invoked. // In that case, if device credentials are allowed, the UI is already showing. If not // allowed, the error has already been returned to the caller. - Slog.w(TAG, "handleOnReadyForAuthentication: AuthSession is null"); + Slog.w(TAG, "handleOnReadyForAuthentication: AuthSession is not current"); return; } - mCurrentAuthSession.onCookieReceived(cookie); + session.onCookieReceived(cookie); } private void handleAuthenticate(IBinder token, long requestId, long operationId, int userId, @@ -1428,47 +1274,41 @@ public class BiometricService extends SystemService { // No need to dismiss dialog / send error yet if we're continuing authentication, e.g. // "Try again" is showing due to something like ERROR_TIMEOUT. - if (mCurrentAuthSession != null) { + if (mAuthSession != null) { // Forcefully cancel authentication. Dismiss the UI, and immediately send // ERROR_CANCELED to the client. Note that we should/will ignore HAL ERROR_CANCELED. // Expect to see some harmless "unknown cookie" errors. - Slog.w(TAG, "Existing AuthSession: " + mCurrentAuthSession); - mCurrentAuthSession.onCancelAuthSession(true /* force */); - mCurrentAuthSession = null; + Slog.w(TAG, "Existing AuthSession: " + mAuthSession); + mAuthSession.onCancelAuthSession(true /* force */); + mAuthSession = null; } final boolean debugEnabled = mInjector.isDebugEnabled(getContext(), userId); - mCurrentAuthSession = new AuthSession(getContext(), mStatusBarService, mSysuiReceiver, - mKeyStore, mRandom, mClientDeathReceiver, preAuthInfo, token, requestId, - operationId, userId, mBiometricSensorReceiver, receiver, opPackageName, promptInfo, - debugEnabled, mInjector.getFingerprintSensorProperties(getContext())); + mAuthSession = new AuthSession(getContext(), mStatusBarService, + createSysuiReceiver(requestId), mKeyStore, mRandom, + createClientDeathReceiver(requestId), preAuthInfo, token, requestId, + operationId, userId, createBiometricSensorReceiver(requestId), receiver, + opPackageName, promptInfo, debugEnabled, + mInjector.getFingerprintSensorProperties(getContext())); try { - mCurrentAuthSession.goToInitialState(); + mAuthSession.goToInitialState(); } catch (RemoteException e) { Slog.e(TAG, "RemoteException", e); } } private void handleCancelAuthentication(long requestId) { - if (mCurrentAuthSession == null) { - Slog.e(TAG, "handleCancelAuthentication: AuthSession is null"); - return; - } - if (mCurrentAuthSession.getRequestId() != requestId) { - // TODO: actually cancel the operation - // This can happen if the operation has been queued, but is cancelled before - // it reaches the head of the scheduler. Consider it a programming error for now - // and ignore it. - Slog.e(TAG, "handleCancelAuthentication: AuthSession mismatch current requestId: " - + mCurrentAuthSession.getRequestId() + " cancel for: " + requestId - + " (ignoring cancellation)"); + final AuthSession session = getAuthSessionIfCurrent(requestId); + if (session == null) { + Slog.w(TAG, "handleCancelAuthentication: AuthSession is not current"); + // TODO: actually cancel the operation? return; } - final boolean finished = mCurrentAuthSession.onCancelAuthSession(false /* force */); + final boolean finished = session.onCancelAuthSession(false /* force */); if (finished) { Slog.d(TAG, "handleCancelAuthentication: AuthSession finished"); - mCurrentAuthSession = null; + mAuthSession = null; } } @@ -1491,7 +1331,7 @@ public class BiometricService extends SystemService { pw.println(" " + sensor); } pw.println(); - pw.println("CurrentSession: " + mCurrentAuthSession); + pw.println("CurrentSession: " + mAuthSession); pw.println(); pw.println("CoexCoordinator: " + CoexCoordinator.getInstance().toString()); pw.println(); diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index 54b79e1f8e4a..6d687726dbe8 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -86,6 +86,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T> private long mStartTimeMs; private boolean mAuthAttempted; + private boolean mAuthSuccess = false; // TODO: This is currently hard to maintain, as each AuthenticationClient subclass must update // the state. We should think of a way to improve this in the future. @@ -237,6 +238,7 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T> "Successful background authentication!"); } + mAuthSuccess = true; markAlreadyDone(); if (mTaskStackListener != null) { @@ -502,6 +504,11 @@ public abstract class AuthenticationClient<T> extends AcquisitionClient<T> return mAuthAttempted; } + /** If an auth attempt completed successfully. */ + public boolean wasAuthSuccessful() { + return mAuthSuccess; + } + protected int getShowOverlayReason() { if (isKeyguard()) { return BiometricOverlayConstants.REASON_AUTH_KEYGUARD; diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java index 1a6da94f683e..d0ec4470d3e6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -316,7 +316,8 @@ public class BiometricScheduler { } } else { try { - mBiometricService.onReadyForAuthentication(cookie); + mBiometricService.onReadyForAuthentication( + mCurrentOperation.getClientMonitor().getRequestId(), cookie); } catch (RemoteException e) { Slog.e(getTag(), "Remote exception when contacting BiometricService", e); } diff --git a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java index f1c786b4977c..46d863d7aaec 100644 --- a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java +++ b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java @@ -123,7 +123,8 @@ public class ClientMonitorCallbackConverter { } } - void onRemoved(BiometricAuthenticator.Identifier identifier, int remaining) + /** Called when a user has been removed. */ + public void onRemoved(BiometricAuthenticator.Identifier identifier, int remaining) throws RemoteException { if (mFaceServiceReceiver != null) { mFaceServiceReceiver.onRemoved((Face) identifier, remaining); diff --git a/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java index 25d4a38cd475..5aa9b79c074c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java +++ b/services/core/java/com/android/server/biometrics/sensors/CoexCoordinator.java @@ -173,18 +173,13 @@ public class CoexCoordinator { } // SensorType to AuthenticationClient map - private final Map<Integer, AuthenticationClient<?>> mClientMap; - @VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths; + private final Map<Integer, AuthenticationClient<?>> mClientMap = new HashMap<>(); + @VisibleForTesting final LinkedList<SuccessfulAuth> mSuccessfulAuths = new LinkedList<>(); private boolean mAdvancedLogicEnabled; private boolean mFaceHapticDisabledWhenNonBypass; - private final Handler mHandler; + private final Handler mHandler = new Handler(Looper.getMainLooper()); - private CoexCoordinator() { - // Singleton - mClientMap = new HashMap<>(); - mSuccessfulAuths = new LinkedList<>(); - mHandler = new Handler(Looper.getMainLooper()); - } + private CoexCoordinator() {} public void addAuthenticationClient(@BiometricScheduler.SensorType int sensorType, @NonNull AuthenticationClient<?> client) { @@ -221,8 +216,14 @@ public class CoexCoordinator { public void onAuthenticationSucceeded(long currentTimeMillis, @NonNull AuthenticationClient<?> client, @NonNull Callback callback) { + final boolean isUsingSingleModality = isSingleAuthOnly(client); + if (client.isBiometricPrompt()) { - callback.sendHapticFeedback(); + if (!isUsingSingleModality && hasMultipleSuccessfulAuthentications()) { + // only send feedback on the first one + } else { + callback.sendHapticFeedback(); + } // For BP, BiometricService will add the authToken to Keystore. callback.sendAuthenticationResult(false /* addAuthTokenIfStrong */); callback.handleLifecycleAfterAuth(); @@ -234,7 +235,7 @@ public class CoexCoordinator { callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); callback.handleLifecycleAfterAuth(); } else if (mAdvancedLogicEnabled && client.isKeyguard()) { - if (isSingleAuthOnly(client)) { + if (isUsingSingleModality) { // Single sensor authentication callback.sendHapticFeedback(); callback.sendAuthenticationResult(true /* addAuthTokenIfStrong */); @@ -295,10 +296,10 @@ public class CoexCoordinator { @NonNull AuthenticationClient<?> client, @LockoutTracker.LockoutMode int lockoutMode, @NonNull Callback callback) { - final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard(); + final boolean isUsingSingleModality = isSingleAuthOnly(client); - if (keyguardAdvancedLogic) { - if (isSingleAuthOnly(client)) { + if (mAdvancedLogicEnabled && client.isKeyguard()) { + if (isUsingSingleModality) { callback.sendHapticFeedback(); callback.handleLifecycleAfterAuth(); } else { @@ -319,8 +320,7 @@ public class CoexCoordinator { // also done now. callback.sendHapticFeedback(); callback.handleLifecycleAfterAuth(); - } - else { + } else { // UDFPS auth has never been attempted. if (mFaceHapticDisabledWhenNonBypass && !face.isKeyguardBypassEnabled()) { Slog.w(TAG, "Skipping face reject haptic"); @@ -360,6 +360,11 @@ public class CoexCoordinator { callback.handleLifecycleAfterAuth(); } } + } else if (client.isBiometricPrompt() && !isUsingSingleModality) { + if (!isCurrentFaceAuth(client)) { + callback.sendHapticFeedback(); + } + callback.handleLifecycleAfterAuth(); } else { callback.sendHapticFeedback(); callback.handleLifecycleAfterAuth(); @@ -380,6 +385,8 @@ public class CoexCoordinator { */ public void onAuthenticationError(@NonNull AuthenticationClient<?> client, @BiometricConstants.Errors int error, @NonNull ErrorCallback callback) { + final boolean isUsingSingleModality = isSingleAuthOnly(client); + // Figure out non-coex state final boolean shouldUsuallyVibrate; if (isCurrentFaceAuth(client)) { @@ -401,25 +408,26 @@ public class CoexCoordinator { } // Figure out coex state - final boolean keyguardAdvancedLogic = mAdvancedLogicEnabled && client.isKeyguard(); final boolean hapticSuppressedByCoex; - - if (keyguardAdvancedLogic) { - if (isSingleAuthOnly(client)) { + if (mAdvancedLogicEnabled && client.isKeyguard()) { + if (isUsingSingleModality) { hapticSuppressedByCoex = false; } else { hapticSuppressedByCoex = isCurrentFaceAuth(client) && !client.isKeyguardBypassEnabled(); } + } else if (client.isBiometricPrompt() && !isUsingSingleModality) { + hapticSuppressedByCoex = isCurrentFaceAuth(client); } else { hapticSuppressedByCoex = false; } // Combine and send feedback if appropriate - Slog.d(TAG, "shouldUsuallyVibrate: " + shouldUsuallyVibrate - + ", hapticSuppressedByCoex: " + hapticSuppressedByCoex); if (shouldUsuallyVibrate && !hapticSuppressedByCoex) { callback.sendHapticFeedback(); + } else { + Slog.v(TAG, "no haptic shouldUsuallyVibrate: " + shouldUsuallyVibrate + + ", hapticSuppressedByCoex: " + hapticSuppressedByCoex); } } @@ -504,6 +512,19 @@ public class CoexCoordinator { return true; } + private boolean hasMultipleSuccessfulAuthentications() { + int count = 0; + for (AuthenticationClient<?> c : mClientMap.values()) { + if (c.wasAuthSuccessful()) { + count++; + } + if (count > 1) { + return true; + } + } + return false; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java index 07ce841a7cac..e0d519469e32 100644 --- a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java @@ -19,6 +19,7 @@ package com.android.server.biometrics.sensors; import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricConstants; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; @@ -71,6 +72,24 @@ public abstract class RemovalClient<S extends BiometricAuthenticator.Identifier, @Override public void onRemoved(@NonNull BiometricAuthenticator.Identifier identifier, int remaining) { + // This happens when we have failed to remove a biometric. + if (identifier == null) { + Slog.e(TAG, "identifier was null, skipping onRemove()"); + try { + if (getListener() != null) { + getListener().onError(getSensorId(), getCookie(), + BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_REMOVE, + 0 /* vendorCode */); + } else { + Slog.e(TAG, "Error, listener was null, not sending onError callback"); + } + } catch (RemoteException e) { + Slog.w(TAG, "Failed to send error to client for onRemoved", e); + } + mCallback.onClientFinished(this, false /* success */); + return; + } + Slog.d(TAG, "onRemoved: " + identifier.getBiometricId() + " remaining: " + remaining); mBiometricUtils.removeBiometricForUser(getContext(), getTargetUserId(), identifier.getBiometricId()); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 9bfdd687644b..66e9da040102 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2579,7 +2579,7 @@ public final class DisplayManagerService extends SystemService { boolean getAllowNonNativeRefreshRateOverride() { return DisplayProperties - .debug_allow_non_native_refresh_rate_override().orElse(false); + .debug_allow_non_native_refresh_rate_override().orElse(true); } @NonNull diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java index a102406cc131..1203769cb72b 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java @@ -262,7 +262,7 @@ public class LockSettingsStrongAuth { long nextAlarmTime = strongAuthTime + dpm.getRequiredStrongAuthTimeout(null, userId); // schedule a new alarm listener for the user - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, + mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextAlarmTime, STRONG_AUTH_TIMEOUT_ALARM_TAG, alarm, mHandler); } @@ -303,7 +303,7 @@ public class LockSettingsStrongAuth { alarm = new NonStrongBiometricTimeoutAlarmListener(userId); mNonStrongBiometricTimeoutAlarmListener.put(userId, alarm); // schedule a new alarm listener for the user - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, + mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextAlarmTime, NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG, alarm, mHandler); } @@ -394,7 +394,7 @@ public class LockSettingsStrongAuth { } // schedule a new alarm listener for the user if (DEBUG) Slog.d(TAG, "Schedule a new alarm for non-strong biometric idle timeout"); - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, + mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextAlarmTime, NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG, alarm, mHandler); } diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index ca3ee8558dd0..0c9855b2385d 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -2812,8 +2812,8 @@ public class ComputerEngine implements Computer { "MATCH_ANY_USER flag requires INTERACT_ACROSS_USERS permission"); } else if ((flags & PackageManager.MATCH_UNINSTALLED_PACKAGES) != 0 && isCallerSystemUser - && mUserManager.hasManagedProfile(UserHandle.USER_SYSTEM)) { - // If the caller wants all packages and has a restricted profile associated with it, + && mUserManager.hasProfile(UserHandle.USER_SYSTEM)) { + // If the caller wants all packages and has a profile associated with it, // then match all users. This is to make sure that launchers that need to access //work // profile apps don't start breaking. TODO: Remove this hack when launchers stop diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 54c201945be6..ceab92577bb3 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2893,9 +2893,13 @@ final class InstallPackageHelper { } } - final boolean deferInstallObserver = succeeded && update && !killApp; + final boolean deferInstallObserver = succeeded && update; if (deferInstallObserver) { - mPm.scheduleDeferredNoKillInstallObserver(res, installObserver); + if (killApp) { + mPm.scheduleDeferredPendingKillInstallObserver(res, installObserver); + } else { + mPm.scheduleDeferredNoKillInstallObserver(res, installObserver); + } } else { mPm.notifyInstallObserver(res, installObserver); } diff --git a/services/core/java/com/android/server/pm/PackageHandler.java b/services/core/java/com/android/server/pm/PackageHandler.java index b028a2cef2a5..e8faca9765f8 100644 --- a/services/core/java/com/android/server/pm/PackageHandler.java +++ b/services/core/java/com/android/server/pm/PackageHandler.java @@ -24,6 +24,7 @@ import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL; import static com.android.server.pm.PackageManagerService.DEFAULT_UNUSED_STATIC_SHARED_LIB_MIN_CACHE_PERIOD; import static com.android.server.pm.PackageManagerService.DEFERRED_NO_KILL_INSTALL_OBSERVER; import static com.android.server.pm.PackageManagerService.DEFERRED_NO_KILL_POST_DELETE; +import static com.android.server.pm.PackageManagerService.DEFERRED_PENDING_KILL_INSTALL_OBSERVER; import static com.android.server.pm.PackageManagerService.DOMAIN_VERIFICATION; import static com.android.server.pm.PackageManagerService.ENABLE_ROLLBACK_STATUS; import static com.android.server.pm.PackageManagerService.ENABLE_ROLLBACK_TIMEOUT; @@ -126,10 +127,12 @@ final class PackageHandler extends Handler { } } } break; - case DEFERRED_NO_KILL_INSTALL_OBSERVER: { - String packageName = (String) msg.obj; + case DEFERRED_NO_KILL_INSTALL_OBSERVER: + case DEFERRED_PENDING_KILL_INSTALL_OBSERVER: { + final String packageName = (String) msg.obj; if (packageName != null) { - mPm.notifyInstallObserver(packageName); + final boolean killApp = msg.what == DEFERRED_PENDING_KILL_INSTALL_OBSERVER; + mPm.notifyInstallObserver(packageName, killApp); } } break; case WRITE_SETTINGS: { diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 0602f3e72031..6fbad24d58c0 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -840,6 +840,9 @@ public class PackageManagerService extends IPackageManager.Stub private final Map<String, Pair<PackageInstalledInfo, IPackageInstallObserver2>> mNoKillInstallObservers = Collections.synchronizedMap(new HashMap<>()); + private final Map<String, Pair<PackageInstalledInfo, IPackageInstallObserver2>> + mPendingKillInstallObservers = Collections.synchronizedMap(new HashMap<>()); + // Internal interface for permission manager final PermissionManagerServiceInternal mPermissionManager; @@ -887,9 +890,11 @@ public class PackageManagerService extends IPackageManager.Stub static final int CHECK_PENDING_INTEGRITY_VERIFICATION = 26; static final int DOMAIN_VERIFICATION = 27; static final int PRUNE_UNUSED_STATIC_SHARED_LIBRARIES = 28; + static final int DEFERRED_PENDING_KILL_INSTALL_OBSERVER = 29; static final int DEFERRED_NO_KILL_POST_DELETE_DELAY_MS = 3 * 1000; private static final int DEFERRED_NO_KILL_INSTALL_OBSERVER_DELAY_MS = 500; + private static final int DEFERRED_PENDING_KILL_INSTALL_OBSERVER_DELAY_MS = 1000; static final int WRITE_SETTINGS_DELAY = 10*1000; // 10 seconds @@ -1166,13 +1171,14 @@ public class PackageManagerService extends IPackageManager.Stub Computer computer = snapshotComputer(); ArraySet<String> packagesToNotify = computer.getNotifyPackagesForReplacedReceived(packages); for (int index = 0; index < packagesToNotify.size(); index++) { - notifyInstallObserver(packagesToNotify.valueAt(index)); + notifyInstallObserver(packagesToNotify.valueAt(index), false /* killApp */); } } - void notifyInstallObserver(String packageName) { - Pair<PackageInstalledInfo, IPackageInstallObserver2> pair = - mNoKillInstallObservers.remove(packageName); + void notifyInstallObserver(String packageName, boolean killApp) { + final Pair<PackageInstalledInfo, IPackageInstallObserver2> pair = + killApp ? mPendingKillInstallObservers.remove(packageName) + : mNoKillInstallObservers.remove(packageName); if (pair != null) { notifyInstallObserver(pair.first, pair.second); @@ -1211,6 +1217,15 @@ public class PackageManagerService extends IPackageManager.Stub delay ? getPruneUnusedSharedLibrariesDelay() : 0); } + void scheduleDeferredPendingKillInstallObserver(PackageInstalledInfo info, + IPackageInstallObserver2 observer) { + final String packageName = info.mPkg.getPackageName(); + mPendingKillInstallObservers.put(packageName, Pair.create(info, observer)); + final Message message = mHandler.obtainMessage(DEFERRED_PENDING_KILL_INSTALL_OBSERVER, + packageName); + mHandler.sendMessageDelayed(message, DEFERRED_PENDING_KILL_INSTALL_OBSERVER_DELAY_MS); + } + private static long getPruneUnusedSharedLibrariesDelay() { return SystemProperties.getLong("debug.pm.prune_unused_shared_libraries_delay", PRUNE_UNUSED_SHARED_LIBRARIES_DELAY); @@ -2055,8 +2070,6 @@ public class PackageManagerService extends IPackageManager.Stub } } - mPrepareAppDataFuture = mAppDataHelper.fixAppsDataOnBoot(); - // If this is first boot after an OTA, and a normal boot, then // we need to clear code cache directories. // Note that we do *not* clear the application profiles. These remain valid @@ -2077,6 +2090,9 @@ public class PackageManagerService extends IPackageManager.Stub ver.fingerprint = PackagePartitions.FINGERPRINT; } + // Defer the app data fixup until we are done with app data clearing above. + mPrepareAppDataFuture = mAppDataHelper.fixAppsDataOnBoot(); + // Legacy existing (installed before Q) non-system apps to hide // their icons in launcher. if (!mOnlyCore && mIsPreQUpgrade) { @@ -2557,8 +2573,9 @@ public class PackageManagerService extends IPackageManager.Stub try { return super.onTransact(code, data, reply, flags); } catch (RuntimeException e) { - if (!(e instanceof SecurityException) && !(e instanceof IllegalArgumentException)) { - Slog.wtf(TAG, "Package Manager Crash", e); + if (!(e instanceof SecurityException) && !(e instanceof IllegalArgumentException) + && !(e instanceof ParcelableException)) { + Slog.wtf(TAG, "Package Manager Unexpected Exception", e); } throw e; } @@ -6675,6 +6692,11 @@ public class PackageManagerService extends IPackageManager.Stub @Override public IPackageInstaller getPackageInstaller() { + // Return installer service for internal calls. + if (PackageManagerServiceUtils.isSystemOrRoot()) { + return mInstallerService; + } + // Return null for InstantApps. if (getInstantAppPackageName(Binder.getCallingUid()) != null) { return null; } @@ -7393,6 +7415,12 @@ public class PackageManagerService extends IPackageManager.Stub } @Override + public void onPackageProcessKilledForUninstall(String packageName) { + mHandler.post(() -> PackageManagerService.this.notifyInstallObserver(packageName, + true /* killApp */)); + } + + @Override public SparseArray<String> getAppsWithSharedUserIds() { return mComputer.getAppsWithSharedUserIds(); } diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index a9471cf57070..8d3fbf7fc679 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -1250,6 +1250,14 @@ public class PackageManagerServiceUtils { } /** + * Check if the Binder caller is system UID or root's UID. + */ + public static boolean isSystemOrRoot() { + final int uid = Binder.getCallingUid(); + return uid == Process.SYSTEM_UID || uid == Process.ROOT_UID; + } + + /** * Enforces that only the system UID or root's UID can call a method exposed * via Binder. * @@ -1257,8 +1265,7 @@ public class PackageManagerServiceUtils { * @throws SecurityException if the caller is not system or root */ public static void enforceSystemOrRoot(String message) { - final int uid = Binder.getCallingUid(); - if (uid != Process.SYSTEM_UID && uid != Process.ROOT_UID) { + if (!isSystemOrRoot()) { throw new SecurityException(message); } } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index b7c55c57c6ce..fed214fd5ab0 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -6263,11 +6263,11 @@ public class UserManagerService extends IUserManager.Stub { } /** - * Checks if the given user has a managed profile associated with it. + * Checks if the given user has a profile associated with it. * @param userId The parent user * @return */ - boolean hasManagedProfile(@UserIdInt int userId) { + boolean hasProfile(@UserIdInt int userId) { synchronized (mUsersLock) { UserInfo userInfo = getUserInfoLU(userId); final int userSize = mUsers.size(); diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java index 49553f4c91f4..36633cc635e7 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -1021,7 +1021,8 @@ final class DefaultPermissionGrantPolicy { } for (String packageName : packageNames) { grantPermissionsToSystemPackage(NO_PM_CACHE, packageName, userId, - PHONE_PERMISSIONS, ALWAYS_LOCATION_PERMISSIONS, SMS_PERMISSIONS); + PHONE_PERMISSIONS, ALWAYS_LOCATION_PERMISSIONS, SMS_PERMISSIONS, + NOTIFICATION_PERMISSIONS); } } diff --git a/services/core/java/com/android/server/pm/pkg/AndroidPackageApi.java b/services/core/java/com/android/server/pm/pkg/AndroidPackageApi.java index d3c8c7b57df2..a0bb8dc48ecf 100644 --- a/services/core/java/com/android/server/pm/pkg/AndroidPackageApi.java +++ b/services/core/java/com/android/server/pm/pkg/AndroidPackageApi.java @@ -268,6 +268,9 @@ public interface AndroidPackageApi { List<FeatureGroupInfo> getFeatureGroups(); @NonNull + List<String> getImplicitPermissions(); + + @NonNull List<ParsedInstrumentation> getInstrumentations(); long getLongVersionCode(); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index bd58472f2d2c..914e5eccd192 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1176,14 +1176,14 @@ public final class PowerManagerService extends SystemService @Override public void onBootPhase(int phase) { - synchronized (mLock) { - if (phase == PHASE_SYSTEM_SERVICES_READY) { - systemReady(); + if (phase == PHASE_SYSTEM_SERVICES_READY) { + systemReady(); - } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { - incrementBootCount(); + } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + incrementBootCount(); - } else if (phase == PHASE_BOOT_COMPLETED) { + } else if (phase == PHASE_BOOT_COMPLETED) { + synchronized (mLock) { final long now = mClock.uptimeMillis(); mBootCompleted = true; mDirty |= DIRTY_BOOT_COMPLETED; diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index 2491565dd376..8755662ac813 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -27,6 +27,7 @@ import android.os.IHintSession; import android.os.Process; import android.os.RemoteException; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -51,8 +52,13 @@ public final class HintManagerService extends SystemService { private static final boolean DEBUG = false; @VisibleForTesting final long mHintSessionPreferredRate; + // Multi-levle map storing all active AppHintSessions. + // First level is keyed by the UID of the client process creating the session. + // Second level is keyed by an IBinder passed from client process. This is used to observe + // when the process exits. The client generally uses the same IBinder object across multiple + // sessions, so the value is a set of AppHintSessions. @GuardedBy("mLock") - private final ArrayMap<Integer, ArrayMap<IBinder, AppHintSession>> mActiveSessions; + private final ArrayMap<Integer, ArrayMap<IBinder, ArraySet<AppHintSession>>> mActiveSessions; /** Lock to protect HAL handles and listen list. */ private final Object mLock = new Object(); @@ -201,13 +207,16 @@ public final class HintManagerService extends SystemService { public void onUidGone(int uid, boolean disabled) { FgThread.getHandler().post(() -> { synchronized (mLock) { - ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(uid); + ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap = mActiveSessions.get(uid); if (tokenMap == null) { return; } for (int i = tokenMap.size() - 1; i >= 0; i--) { // Will remove the session from tokenMap - tokenMap.valueAt(i).close(); + ArraySet<AppHintSession> sessionSet = tokenMap.valueAt(i); + for (int j = sessionSet.size() - 1; j >= 0; j--) { + sessionSet.valueAt(j).close(); + } } mProcStatesCache.delete(uid); } @@ -231,12 +240,14 @@ public final class HintManagerService extends SystemService { FgThread.getHandler().post(() -> { synchronized (mLock) { mProcStatesCache.put(uid, procState); - ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(uid); + ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap = mActiveSessions.get(uid); if (tokenMap == null) { return; } - for (AppHintSession s : tokenMap.values()) { - s.onProcStateChanged(); + for (ArraySet<AppHintSession> sessionSet : tokenMap.values()) { + for (AppHintSession s : sessionSet) { + s.onProcStateChanged(); + } } } }); @@ -305,17 +316,25 @@ public final class HintManagerService extends SystemService { long halSessionPtr = mNativeWrapper.halCreateHintSession(callingTgid, callingUid, tids, durationNanos); - if (halSessionPtr == 0) return null; + if (halSessionPtr == 0) { + return null; + } AppHintSession hs = new AppHintSession(callingUid, callingTgid, tids, token, halSessionPtr, durationNanos); synchronized (mLock) { - ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(callingUid); + ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap = + mActiveSessions.get(callingUid); if (tokenMap == null) { tokenMap = new ArrayMap<>(1); mActiveSessions.put(callingUid, tokenMap); } - tokenMap.put(token, hs); + ArraySet<AppHintSession> sessionSet = tokenMap.get(token); + if (sessionSet == null) { + sessionSet = new ArraySet<>(1); + tokenMap.put(token, sessionSet); + } + sessionSet.add(hs); return hs; } } finally { @@ -339,10 +358,14 @@ public final class HintManagerService extends SystemService { pw.println("Active Sessions:"); for (int i = 0; i < mActiveSessions.size(); i++) { pw.println("Uid " + mActiveSessions.keyAt(i).toString() + ":"); - ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.valueAt(i); + ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap = + mActiveSessions.valueAt(i); for (int j = 0; j < tokenMap.size(); j++) { - pw.println(" Session " + j + ":"); - tokenMap.valueAt(j).dump(pw, " "); + ArraySet<AppHintSession> sessionSet = tokenMap.valueAt(j); + for (int k = 0; k < sessionSet.size(); ++k) { + pw.println(" Session:"); + sessionSet.valueAt(k).dump(pw, " "); + } } } } @@ -432,11 +455,18 @@ public final class HintManagerService extends SystemService { mNativeWrapper.halCloseHintSession(mHalSessionPtr); mHalSessionPtr = 0; mToken.unlinkToDeath(this, 0); - ArrayMap<IBinder, AppHintSession> tokenMap = mActiveSessions.get(mUid); + ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap = mActiveSessions.get(mUid); if (tokenMap == null) { - Slogf.w(TAG, "UID %d is note present in active session map", mUid); + Slogf.w(TAG, "UID %d is not present in active session map", mUid); + return; + } + ArraySet<AppHintSession> sessionSet = tokenMap.get(mToken); + if (sessionSet == null) { + Slogf.w(TAG, "Token %s is not present in token map", mToken.toString()); + return; } - tokenMap.remove(mToken); + sessionSet.remove(this); + if (sessionSet.isEmpty()) tokenMap.remove(mToken); if (tokenMap.isEmpty()) mActiveSessions.remove(mUid); } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index 411f3dcc1eb6..11fd99cf5b68 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -157,7 +157,7 @@ public interface StatusBarManagerInternal { * @see com.android.internal.statusbar.IStatusBar#requestWindowMagnificationConnection(boolean * request) */ - void requestWindowMagnificationConnection(boolean request); + boolean requestWindowMagnificationConnection(boolean request); /** * Handles a logging command from the WM shell command. diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index e4a969b3ca1d..59b9daf709d8 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -637,12 +637,14 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void requestWindowMagnificationConnection(boolean request) { + public boolean requestWindowMagnificationConnection(boolean request) { if (mBar != null) { try { mBar.requestWindowMagnificationConnection(request); + return true; } catch (RemoteException ex) { } } + return false; } @Override @@ -856,11 +858,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void onBiometricAuthenticated() { + public void onBiometricAuthenticated(@Modality int modality) { enforceBiometricDialog(); if (mBar != null) { try { - mBar.onBiometricAuthenticated(); + mBar.onBiometricAuthenticated(modality); } catch (RemoteException ex) { } } @@ -1952,8 +1954,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D public void setNavBarMode(@NavBarMode int navBarMode) { enforceStatusBar(); if (navBarMode != NAV_BAR_MODE_DEFAULT && navBarMode != NAV_BAR_MODE_KIDS) { - throw new UnsupportedOperationException( - "Supplied navBarMode not supported: " + navBarMode); + throw new IllegalArgumentException("Supplied navBarMode not supported: " + navBarMode); } final int userId = mCurrentUserId; @@ -1961,6 +1962,8 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D try { Settings.Secure.putIntForUser(mContext.getContentResolver(), Settings.Secure.NAV_BAR_KIDS_MODE, navBarMode, userId); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.NAV_BAR_FORCE_VISIBLE, navBarMode, userId); IOverlayManager overlayManager = getOverlayManager(); if (overlayManager != null && navBarMode == NAV_BAR_MODE_KIDS diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 8f703c5c7761..d3f3abe1085d 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -79,6 +79,7 @@ import android.util.ArraySet; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.TypedValue; import android.util.proto.ProtoOutputStream; import android.view.Display; @@ -138,7 +139,7 @@ final class AccessibilityController { new SparseArray<>(); private SparseArray<IBinder> mFocusedWindow = new SparseArray<>(); private int mFocusedDisplay = -1; - private boolean mIsImeVisible = false; + private final SparseBooleanArray mIsImeVisibleArray = new SparseBooleanArray(); // Set to true if initializing window population complete. private boolean mAllObserversInitialized = true; private final AccessibilityWindowsPopulator mAccessibilityWindowsPopulator; @@ -167,8 +168,11 @@ final class AccessibilityController { if (dc != null) { final Display display = dc.getDisplay(); if (display != null && display.getType() != Display.TYPE_OVERLAY) { - mDisplayMagnifiers.put(displayId, new DisplayMagnifier( - mService, dc, display, callbacks)); + final DisplayMagnifier magnifier = new DisplayMagnifier( + mService, dc, display, callbacks); + magnifier.notifyImeWindowVisibilityChanged( + mIsImeVisibleArray.get(displayId, false)); + mDisplayMagnifiers.put(displayId, magnifier); result = true; } } @@ -494,11 +498,13 @@ final class AccessibilityController { mAccessibilityTracing.logTrace(TAG + ".updateImeVisibilityIfNeeded", FLAGS_MAGNIFICATION_CALLBACK, "displayId=" + displayId + ";shown=" + shown); } - if (mIsImeVisible == shown) { + + final boolean isDisplayImeVisible = mIsImeVisibleArray.get(displayId, false); + if (isDisplayImeVisible == shown) { return; } - mIsImeVisible = shown; + mIsImeVisibleArray.put(displayId, shown); final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId); if (displayMagnifier != null) { displayMagnifier.notifyImeWindowVisibilityChanged(shown); @@ -534,6 +540,7 @@ final class AccessibilityController { } public void onDisplayRemoved(int displayId) { + mIsImeVisibleArray.delete(displayId); mFocusedWindow.remove(displayId); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9c910eb730ab..4e751f5a3ab3 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -46,7 +46,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.activityTypeToString; -import static android.app.WindowConfiguration.isSplitScreenWindowingMode; import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION; import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE; import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; @@ -7515,8 +7514,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final int parentWindowingMode = newParentConfiguration.windowConfiguration.getWindowingMode(); final boolean isFixedOrientationLetterboxAllowed = - isSplitScreenWindowingMode(parentWindowingMode) - || parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW + parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW || parentWindowingMode == WINDOWING_MODE_FULLSCREEN; // TODO(b/181207944): Consider removing the if condition and always run // resolveFixedOrientationConfiguration() since this should be applied for all cases. diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 487aff63b555..23f14a7b7a16 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -53,7 +53,7 @@ class BackNavigationController { * Returns true if the back predictability feature is enabled */ static boolean isEnabled() { - return SystemProperties.getInt(BACK_PREDICTABILITY_PROP, 1) > 0; + return SystemProperties.getInt(BACK_PREDICTABILITY_PROP, 0) > 0; } static boolean isScreenshotEnabled() { diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java index afa4f190c6e3..08a9da467162 100644 --- a/services/core/java/com/android/server/wm/ConfigurationContainer.java +++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java @@ -24,8 +24,6 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; import static android.app.WindowConfiguration.activityTypeToString; import static android.app.WindowConfiguration.windowingModeToString; import static android.app.WindowConfigurationProto.WINDOWING_MODE; @@ -475,33 +473,9 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> { return WindowConfiguration.inMultiWindowMode(windowingMode); } - /** Returns true if this container is currently in split-screen windowing mode. */ - public boolean inSplitScreenWindowingMode() { - /*@WindowConfiguration.WindowingMode*/ int windowingMode = - mFullConfiguration.windowConfiguration.getWindowingMode(); - - return windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY - || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; - } - - /** Returns true if this container is currently in split-screen secondary windowing mode. */ - public boolean inSplitScreenSecondaryWindowingMode() { - /*@WindowConfiguration.WindowingMode*/ int windowingMode = - mFullConfiguration.windowConfiguration.getWindowingMode(); - - return windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; - } - - public boolean inSplitScreenPrimaryWindowingMode() { - return mFullConfiguration.windowConfiguration.getWindowingMode() - == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; - } - /** - * Returns true if this container can be put in either - * {@link WindowConfiguration#WINDOWING_MODE_SPLIT_SCREEN_PRIMARY} or - * {@link WindowConfiguration##WINDOWING_MODE_SPLIT_SCREEN_SECONDARY} windowing modes based on - * its current state. + * Returns true if this container supports split-screen multi-window and can be put in + * split-screen based on its current state. */ public boolean supportsSplitScreenWindowingMode() { return mFullConfiguration.windowConfiguration.supportSplitScreenWindowingMode(); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 1c0687a351e4..82ad861d85cf 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -26,7 +26,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; @@ -5521,11 +5520,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final float[] tmpFloat9 = new float[9]; forAllWindows(w -> { if (w.isVisible() && !w.inPinnedWindowingMode()) { - if (w.mSession.mSetsUnrestrictedKeepClearAreas) { - outUnrestricted.addAll(w.getKeepClearAreas(tmpMatrix, tmpFloat9)); - } else { - outRestricted.addAll(w.getKeepClearAreas(tmpMatrix, tmpFloat9)); - } + w.getKeepClearAreas(outRestricted, outUnrestricted, tmpMatrix, tmpFloat9); } // We stop traversing when we reach the base of a fullscreen app. @@ -5583,13 +5578,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } static boolean alwaysCreateRootTask(int windowingMode, int activityType) { - // Always create a root task for fullscreen, freeform, and split-screen-secondary windowing + // Always create a root task for fullscreen, freeform, and multi windowing // modes so that we can manage visual ordering and return types correctly. return activityType == ACTIVITY_TYPE_STANDARD && (windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_FREEFORM || windowingMode == WINDOWING_MODE_PINNED - || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY || windowingMode == WINDOWING_MODE_MULTI_WINDOW); } @@ -6368,10 +6362,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** - * Returns the fixed orientation requested by a transient launch (e.g. recents animation). - * If it doesn't return SCREEN_ORIENTATION_UNSET, the rotation change should be deferred. + * Returns {@code true} if the transient launch (e.g. recents animation) requested a fixed + * orientation, then the rotation change should be deferred. */ - @ActivityInfo.ScreenOrientation int getTransientFixedOrientation() { + boolean shouldDeferRotation() { ActivityRecord source = null; if (mTransitionController.isShellTransitionsEnabled()) { final ActivityRecord r = mFixedRotationLaunchingApp; @@ -6383,13 +6377,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } if (source == null || source.getRequestedConfigurationOrientation( true /* forDisplay */) == ORIENTATION_UNDEFINED) { - return SCREEN_ORIENTATION_UNSET; - } - if (!mWmService.mPolicy.okToAnimate(false /* ignoreScreenOn */)) { - // If screen is off or the device is going to sleep, then still allow to update. - return SCREEN_ORIENTATION_UNSET; + return false; } - return source.mOrientation; + // If screen is off or the device is going to sleep, then still allow to update. + return mWmService.mPolicy.okToAnimate(false /* ignoreScreenOn */); } @Override diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index f116fffa8075..262ddae02765 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -446,12 +446,10 @@ public class DisplayRotation { return false; } - final int transientFixedOrientation = - mDisplayContent.mFixedRotationTransitionListener.getTransientFixedOrientation(); - if (transientFixedOrientation != SCREEN_ORIENTATION_UNSET) { + if (mDisplayContent.mFixedRotationTransitionListener.shouldDeferRotation()) { // Makes sure that after the transition is finished, updateOrientation() can see // the difference from the latest orientation source. - mLastOrientation = transientFixedOrientation; + mLastOrientation = SCREEN_ORIENTATION_UNSET; // During the recents animation, the closing app might still be considered on top. // In order to ignore its requested orientation to avoid a sensor led rotation (e.g // user rotating the device while the recents animation is running), we ignore diff --git a/services/core/java/com/android/server/wm/DragResizeMode.java b/services/core/java/com/android/server/wm/DragResizeMode.java index d754fd861e27..684cf06e08b8 100644 --- a/services/core/java/com/android/server/wm/DragResizeMode.java +++ b/services/core/java/com/android/server/wm/DragResizeMode.java @@ -40,8 +40,6 @@ class DragResizeMode { switch (mode) { case DRAG_RESIZE_MODE_FREEFORM: return rootTask.getWindowingMode() == WINDOWING_MODE_FREEFORM; - case DRAG_RESIZE_MODE_DOCKED_DIVIDER: - return rootTask.inSplitScreenWindowingMode(); default: return false; } diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index bb6d83c230ac..4fdb1f7930f5 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -449,11 +449,8 @@ class InsetsPolicy { boolean copyState) { final WindowState roundedCornerWindow = mPolicy.getRoundedCornerWindow(); final Task task = w.getTask(); - final boolean isInSplitScreenMode = task != null && task.inMultiWindowMode() - && task.getRootTask() != null - && task.getRootTask().getAdjacentTaskFragment() != null; if (task != null && !task.getWindowConfiguration().tasksAreFloating() - && (roundedCornerWindow != null || isInSplitScreenMode)) { + && (roundedCornerWindow != null || task.inSplitScreen())) { // Instead of using display frame to calculating rounded corner, for the fake rounded // corners drawn by divider bar or task bar, we need to re-calculate rounded corners // based on task bounds and if the task bounds is intersected with task bar, we should diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java index 93714e881441..a407021c7594 100644 --- a/services/core/java/com/android/server/wm/RecentsAnimationController.java +++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java @@ -18,7 +18,6 @@ package com.android.server.wm; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.RemoteAnimationTarget.MODE_CLOSING; @@ -610,8 +609,7 @@ public class RecentsAnimationController implements DeathRecipient { final TaskAnimationAdapter adapter = mPendingAnimations.get(i); final Task task = adapter.mTask; final TaskFragment adjacentTask = task.getRootTask().getAdjacentTaskFragment(); - final boolean inSplitScreen = task.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW - && adjacentTask != null; + final boolean inSplitScreen = task.inSplitScreen(); if (task.isActivityTypeHomeOrRecents() // Skip if the task is in split screen and in landscape. || (inSplitScreen && isDisplayLandscape) diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index cd8ddf4e9211..bafdd92c058a 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -84,6 +84,7 @@ import com.android.server.LocalServices; import com.android.server.wm.WindowManagerService.H; import java.io.PrintWriter; +import java.util.Collections; import java.util.List; import java.util.function.BiConsumer; @@ -521,10 +522,15 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } @Override - public void reportKeepClearAreasChanged(IWindow window, List<Rect> keepClearAreas) { + public void reportKeepClearAreasChanged(IWindow window, List<Rect> restricted, + List<Rect> unrestricted) { + if (!mSetsUnrestrictedKeepClearAreas && !unrestricted.isEmpty()) { + unrestricted = Collections.emptyList(); + } + final long ident = Binder.clearCallingIdentity(); try { - mService.reportKeepClearAreasChanged(this, window, keepClearAreas); + mService.reportKeepClearAreasChanged(this, window, restricted, unrestricted); } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index f3933c5f7bd6..7fe34f47c53c 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -29,6 +29,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.PINNED_WINDOWING_MODE_ELEVATION_IN_DIP; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.activityTypeToString; @@ -1722,6 +1723,13 @@ class Task extends TaskFragment { && (topTask == null || topTask.supportsSplitScreenWindowingModeInner(tda)); } + /** Returns {@code true} if this task is currently in split-screen. */ + boolean inSplitScreen() { + return getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW + && getRootTask() != null + && getRootTask().getAdjacentTaskFragment() != null; + } + private boolean supportsSplitScreenWindowingModeInner(@Nullable TaskDisplayArea tda) { return super.supportsSplitScreenWindowingMode() && mAtmService.mSupportsSplitScreenMultiWindow @@ -6020,9 +6028,6 @@ class Task extends TaskFragment { } boolean shouldIgnoreInput() { - if (inSplitScreenPrimaryWindowingMode() && !isFocusable()) { - return true; - } if (mAtmService.mHasLeanbackFeature && inPinnedWindowingMode() && !isFocusedRootTaskOnDisplay()) { // Preventing Picture-in-Picture root task from receiving input on TVs. diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 7fab94cab413..afc3087f4ee9 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1767,8 +1767,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { // Resolve override windowing mode to fullscreen for home task (even on freeform // display), or split-screen if in split-screen mode. if (getActivityType() == ACTIVITY_TYPE_HOME && windowingMode == WINDOWING_MODE_UNDEFINED) { - windowingMode = WindowConfiguration.isSplitScreenWindowingMode(parentWindowingMode) - ? parentWindowingMode : WINDOWING_MODE_FULLSCREEN; + windowingMode = WINDOWING_MODE_FULLSCREEN; getResolvedOverrideConfiguration().windowConfiguration.setWindowingMode(windowingMode); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 5eb81872efb1..03e21405ff03 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -4412,10 +4412,11 @@ public class WindowManagerService extends IWindowManager.Stub } } - void reportKeepClearAreasChanged(Session session, IWindow window, List<Rect> keepClearAreas) { + void reportKeepClearAreasChanged(Session session, IWindow window, + List<Rect> restricted, List<Rect> unrestricted) { synchronized (mGlobalLock) { final WindowState win = windowForClientLocked(session, window, true); - if (win.setKeepClearAreas(keepClearAreas)) { + if (win.setKeepClearAreas(restricted, unrestricted)) { win.getDisplayContent().updateKeepClearAreas(); } } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 517837c3732a..d547275dcf0c 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -22,7 +22,6 @@ import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.AppOpsManager.OP_NONE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.isSplitScreenWindowingMode; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.graphics.GraphicsProtos.dumpPointProto; import static android.hardware.display.DisplayManager.SWITCHING_TYPE_NONE; @@ -181,6 +180,7 @@ import static com.android.server.wm.WindowStateProto.REQUESTED_WIDTH; import static com.android.server.wm.WindowStateProto.STACK_ID; import static com.android.server.wm.WindowStateProto.SURFACE_INSETS; import static com.android.server.wm.WindowStateProto.SURFACE_POSITION; +import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_AREAS; import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY; import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER; import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES; @@ -482,6 +482,12 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP */ private final List<Rect> mKeepClearAreas = new ArrayList<>(); + /** + * Like mKeepClearAreas, but the unrestricted ones can be trusted to behave nicely. + * Floating windows (like Pip) will be moved away from them without applying restrictions. + */ + private final List<Rect> mUnrestrictedKeepClearAreas = new ArrayList<>(); + // 0 = left, 1 = right private final int[] mLastRequestedExclusionHeight = {0, 0}; private final int[] mLastGrantedExclusionHeight = {0, 0}; @@ -1023,51 +1029,86 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } /** - * @return a list of rects that should ideally not be covered by floating windows like pip. - * The returned rect coordinates are relative to the display origin. + * Collects all restricted and unrestricted keep-clear areas for this window. + * Keep-clear areas are rects that should ideally not be covered by floating windows like Pip. + * The system is more careful about restricted ones and may apply restrictions to them, while + * the unrestricted ones are considered safe. + * + * @param outRestricted list to add restricted keep-clear areas to + * @param outUnrestricted list to add unrestricted keep-clear areas to */ - List<Rect> getKeepClearAreas() { + void getKeepClearAreas(List<Rect> outRestricted, List<Rect> outUnrestricted) { final Matrix tmpMatrix = new Matrix(); final float[] tmpFloat9 = new float[9]; - return getKeepClearAreas(tmpMatrix, tmpFloat9); + getKeepClearAreas(outRestricted, outUnrestricted, tmpMatrix, tmpFloat9); } /** + * Collects all restricted and unrestricted keep-clear areas for this window. + * Keep-clear areas are rects that should ideally not be covered by floating windows like Pip. + * The system is more careful about restricted ones and may apply restrictions to them, while + * the unrestricted ones are considered safe. + * + * @param outRestricted list to add restricted keep-clear areas to + * @param outUnrestricted list to add unrestricted keep-clear areas to * @param tmpMatrix a temporary matrix to be used for transformations * @param float9 a temporary array of 9 floats - * - * @return a list of rects that should ideally not be covered by floating windows like pip. - * The returned rect coordinates are relative to the display origin. */ - List<Rect> getKeepClearAreas(Matrix tmpMatrix, float[] float9) { + void getKeepClearAreas(List<Rect> outRestricted, List<Rect> outUnrestricted, Matrix tmpMatrix, + float[] float9) { + outRestricted.addAll(getRectsInScreenSpace(mKeepClearAreas, tmpMatrix, float9)); + outUnrestricted.addAll( + getRectsInScreenSpace(mUnrestrictedKeepClearAreas, tmpMatrix, float9)); + } + + /** + * Transforms the given rects from window coordinate space to screen space. + */ + List<Rect> getRectsInScreenSpace(List<Rect> rects, Matrix tmpMatrix, float[] float9) { getTransformationMatrix(float9, tmpMatrix); - // Translate all keep-clear rects to screen coordinates. - final List<Rect> transformedKeepClearAreas = new ArrayList<Rect>(); + final List<Rect> transformedRects = new ArrayList<Rect>(); final RectF tmpRect = new RectF(); Rect curr; - for (Rect r : mKeepClearAreas) { + for (Rect r : rects) { tmpRect.set(r); tmpMatrix.mapRect(tmpRect); curr = new Rect(); tmpRect.roundOut(curr); - transformedKeepClearAreas.add(curr); + transformedRects.add(curr); } - return transformedKeepClearAreas; + return transformedRects; } /** - * @param keepClearAreas the new keep-clear areas for this window. The rects should be defined - * in window coordinate space + * Sets the new keep-clear areas for this window. The rects should be defined in window + * coordinate space. + * Keep-clear areas can be restricted or unrestricted, depending on whether the app holds the + * {@link android.Manifest.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS} system permission. + * Restricted ones will be handled more carefully by the system. Restrictions may be applied. + * Unrestricted ones are considered safe. The system should move floating windows away from them + * without applying restrictions. + * + * @param restricted the new restricted keep-clear areas for this window + * @param unrestricted the new unrestricted keep-clear areas for this window * * @return true if there is a change in the list of keep-clear areas; false otherwise */ - boolean setKeepClearAreas(List<Rect> keepClearAreas) { - if (mKeepClearAreas.equals(keepClearAreas)) { + boolean setKeepClearAreas(List<Rect> restricted, List<Rect> unrestricted) { + final boolean newRestrictedAreas = !mKeepClearAreas.equals(restricted); + final boolean newUnrestrictedAreas = !mUnrestrictedKeepClearAreas.equals(unrestricted); + if (!newRestrictedAreas && !newUnrestrictedAreas) { return false; } - mKeepClearAreas.clear(); - mKeepClearAreas.addAll(keepClearAreas); + if (newRestrictedAreas) { + mKeepClearAreas.clear(); + mKeepClearAreas.addAll(restricted); + } + + if (newUnrestrictedAreas) { + mUnrestrictedKeepClearAreas.clear(); + mUnrestrictedKeepClearAreas.addAll(unrestricted); + } return true; } @@ -3582,11 +3623,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final int requested = mLastRequestedExclusionHeight[side]; final int granted = mLastGrantedExclusionHeight[side]; + final boolean inSplitScreen = getTask() != null && getTask().inSplitScreen(); + FrameworkStatsLog.write(FrameworkStatsLog.EXCLUSION_RECT_STATE_CHANGED, mAttrs.packageName, requested, requested - granted /* rejected */, side + 1 /* Sides are 1-indexed in atoms.proto */, (getConfiguration().orientation == ORIENTATION_LANDSCAPE), - isSplitScreenWindowingMode(getWindowingMode()), (int) duration); + inSplitScreen, (int) duration); } private void initExclusionRestrictions() { @@ -4121,8 +4164,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (task == null) { return false; } - if (!inSplitScreenWindowingMode() && !inFreeformWindowingMode() - && !task.getRootTask().mCreatedByOrganizer) { + if (!inFreeformWindowingMode() && !task.getRootTask().mCreatedByOrganizer) { return false; } // TODO(157912944): formalize drag-resizing so that exceptions aren't hardcoded like this @@ -4202,9 +4244,12 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP proto.write(FORCE_SEAMLESS_ROTATION, mForceSeamlesslyRotate); proto.write(HAS_COMPAT_SCALE, hasCompatScale()); proto.write(GLOBAL_SCALE, mGlobalScale); - for (Rect r : getKeepClearAreas()) { + for (Rect r : mKeepClearAreas) { r.dumpDebug(proto, KEEP_CLEAR_AREAS); } + for (Rect r : mUnrestrictedKeepClearAreas) { + r.dumpDebug(proto, UNRESTRICTED_KEEP_CLEAR_AREAS); + } proto.end(token); } @@ -4373,7 +4418,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } pw.println(prefix + "isOnScreen=" + isOnScreen()); pw.println(prefix + "isVisible=" + isVisible()); - pw.println(prefix + "keepClearAreas=" + getKeepClearAreas()); + pw.println(prefix + "keepClearAreas: restricted=" + mKeepClearAreas + + ", unrestricted=" + mUnrestrictedKeepClearAreas); if (dumpAll) { final String visibilityString = mRequestedVisibilities.toString(); if (!visibilityString.isEmpty()) { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java index 26b5218d2ab4..4a40b5f2de7b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationExitInfoTest.java @@ -1000,7 +1000,7 @@ public class ApplicationExitInfoTest { final String dummyPackageName = "com.android.test"; final String dummyClassName = ".Foo"; app.setHostingRecord(HostingRecord.byAppZygote(new ComponentName( - dummyPackageName, dummyClassName), "", definingUid)); + dummyPackageName, dummyClassName), "", definingUid, "")); } app.mServices.setConnectionGroup(connectionGroup); app.mState.setReportedProcState(procState); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 953b5368c86f..1f016fb6f017 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -154,6 +154,7 @@ public class AccessibilityManagerServiceTest { mMockWindowMagnificationMgr); when(mMockMagnificationController.getFullScreenMagnificationController()).thenReturn( mMockFullScreenMagnificationController); + when(mMockMagnificationController.supportWindowMagnification()).thenReturn(true); when(mMockWindowManagerService.getAccessibilityController()).thenReturn( mMockA11yController); when(mMockA11yController.isAccessibilityTracingEnabled()).thenReturn(false); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java index 3ce2ed84d3e8..f3a0b7fa1ea7 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java @@ -1158,7 +1158,7 @@ public class FullScreenMagnificationControllerTest { MagnificationCallbacks callbacks = getMagnificationCallbacks(DISPLAY_0); callbacks.onImeWindowVisibilityChanged(true); mMessageCapturingHandler.sendAllMessages(); - verify(mRequestObserver).onImeWindowVisibilityChanged(eq(true)); + verify(mRequestObserver).onImeWindowVisibilityChanged(eq(DISPLAY_0), eq(true)); } private void setScaleToMagnifying() { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java index ec59090240f3..cc6d7611b4a1 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java @@ -41,6 +41,7 @@ import static org.mockito.Mockito.when; import android.accessibilityservice.MagnificationConfig; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; @@ -100,6 +101,8 @@ public class MagnificationControllerTest { @Mock private Context mContext; @Mock + PackageManager mPackageManager; + @Mock private FullScreenMagnificationController mScreenMagnificationController; private MagnificationScaleProvider mScaleProvider; @Captor @@ -136,6 +139,7 @@ public class MagnificationControllerTest { mMockResolver = new MockContentResolver(); mMockResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); when(mContext.getContentResolver()).thenReturn(mMockResolver); + when(mContext.getPackageManager()).thenReturn(mPackageManager); Settings.Secure.putFloatForUser(mMockResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, DEFAULT_SCALE, CURRENT_USER_ID); @@ -748,7 +752,7 @@ public class MagnificationControllerTest { MagnificationController spyController = spy(mMagnificationController); spyController.onWindowMagnificationActivationState(TEST_DISPLAY, true); - spyController.onImeWindowVisibilityChanged(true); + spyController.onImeWindowVisibilityChanged(TEST_DISPLAY, true); verify(spyController).logMagnificationModeWithIme( eq(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW)); @@ -759,7 +763,7 @@ public class MagnificationControllerTest { MagnificationController spyController = spy(mMagnificationController); spyController.onFullScreenMagnificationActivationState(TEST_DISPLAY, true); - spyController.onImeWindowVisibilityChanged(true); + spyController.onImeWindowVisibilityChanged(TEST_DISPLAY, true); verify(spyController).logMagnificationModeWithIme( eq(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN)); @@ -768,7 +772,7 @@ public class MagnificationControllerTest { @Test public void imeWindowStateShown_noMagnifying_noLogAnyMode() { MagnificationController spyController = spy(mMagnificationController); - spyController.onImeWindowVisibilityChanged(true); + spyController.onImeWindowVisibilityChanged(TEST_DISPLAY, true); verify(spyController, never()).logMagnificationModeWithIme(anyInt()); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java index 3822dc362b6b..4b77764c41e5 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationConnectionWrapperTest.java @@ -94,6 +94,14 @@ public class WindowMagnificationConnectionWrapperTest { } @Test + public void moveWindowMagnifierToPosition() throws RemoteException { + mConnectionWrapper.moveWindowMagnifierToPosition(TEST_DISPLAY, 100, 150, + mAnimationCallback); + verify(mConnection).moveWindowMagnifierToPosition(eq(TEST_DISPLAY), + eq(100f), eq(150f), any(IRemoteMagnificationAnimationCallback.class)); + } + + @Test public void showMagnificationButton() throws RemoteException { mConnectionWrapper.showMagnificationButton(TEST_DISPLAY, Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java index 0742c09492f2..978000aa89d3 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java @@ -21,6 +21,8 @@ import static com.android.server.accessibility.magnification.MockWindowMagnifica import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.doAnswer; @@ -54,6 +56,8 @@ import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.view.accessibility.IWindowMagnificationConnectionCallback; import android.view.accessibility.MagnificationAnimationCallback; +import androidx.test.core.app.ApplicationProvider; + import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityTraceManager; @@ -99,12 +103,7 @@ public class WindowMagnificationManagerTest { mMockCallback, mMockTrace, new MagnificationScaleProvider(mContext)); when(mContext.getContentResolver()).thenReturn(mResolver); - doAnswer((InvocationOnMock invocation) -> { - final boolean connect = (Boolean) invocation.getArguments()[0]; - mWindowMagnificationManager.setConnection( - connect ? mMockConnection.getConnection() : null); - return null; - }).when(mMockStatusBarManagerInternal).requestWindowMagnificationConnection(anyBoolean()); + stubSetConnection(false); mResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); Settings.Secure.putFloatForUser(mResolver, @@ -112,6 +111,25 @@ public class WindowMagnificationManagerTest { CURRENT_USER_ID); } + private void stubSetConnection(boolean needDelay) { + doAnswer((InvocationOnMock invocation) -> { + final boolean connect = (Boolean) invocation.getArguments()[0]; + // Simulates setConnection() called by another process. + if (needDelay) { + final Context context = ApplicationProvider.getApplicationContext(); + context.getMainThreadHandler().postDelayed( + () -> { + mWindowMagnificationManager.setConnection( + connect ? mMockConnection.getConnection() : null); + }, 10); + } else { + mWindowMagnificationManager.setConnection( + connect ? mMockConnection.getConnection() : null); + } + return true; + }).when(mMockStatusBarManagerInternal).requestWindowMagnificationConnection(anyBoolean()); + } + @Test public void setConnection_connectionIsNull_wrapperIsNullAndLinkToDeath() { mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); @@ -275,32 +293,33 @@ public class WindowMagnificationManagerTest { } @Test - public void onRectangleOnScreenRequested_trackingDisabledByOnDrag_withoutMovingMagnification() + public void onRectangleOnScreenRequested_trackingDisabledByOnDrag_withoutMovingMagnifier() throws RemoteException { mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, 50f, 50f); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, true); final Region outRegion = new Region(); mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, outRegion); final Rect requestedRect = outRegion.getBounds(); requestedRect.offsetTo(requestedRect.right + 10, requestedRect.bottom + 10); - mMockConnection.getConnectionCallback().onDrag(TEST_DISPLAY); + mMockConnection.getConnectionCallback().onMove(TEST_DISPLAY); mWindowMagnificationManager.onRectangleOnScreenRequested(TEST_DISPLAY, requestedRect.left, requestedRect.top, requestedRect.right, requestedRect.bottom); - verify(mMockConnection.getConnection(), never()).enableWindowMagnification(eq(TEST_DISPLAY), - eq(3f), eq(requestedRect.exactCenterX()), eq(requestedRect.exactCenterY()), - eq(0f), eq(0f), notNull()); + verify(mMockConnection.getConnection(), never()) + .moveWindowMagnifierToPosition(anyInt(), anyFloat(), anyFloat(), any()); } @Test - public void onRectangleOnScreenRequested_trackingDisabledByScroll_withoutMovingMagnification() + public void onRectangleOnScreenRequested_trackingDisabledByScroll_withoutMovingMagnifier() throws RemoteException { final float distanceX = 10f; final float distanceY = 10f; mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, 50f, 50f); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, true); final Region outRegion = new Region(); mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, outRegion); final Rect requestedRect = outRegion.getBounds(); @@ -310,16 +329,16 @@ public class WindowMagnificationManagerTest { mWindowMagnificationManager.onRectangleOnScreenRequested(TEST_DISPLAY, requestedRect.left, requestedRect.top, requestedRect.right, requestedRect.bottom); - verify(mMockConnection.getConnection(), never()).enableWindowMagnification(eq(TEST_DISPLAY), - eq(3f), eq(requestedRect.exactCenterX()), eq(requestedRect.exactCenterY()), - eq(0f), eq(0f), notNull()); + verify(mMockConnection.getConnection(), never()) + .moveWindowMagnifierToPosition(anyInt(), anyFloat(), anyFloat(), any()); } @Test - public void onRectangleOnScreenRequested_requestRectangleInBound_withoutMovingMagnification() + public void onRectangleOnScreenRequested_requestRectangleInBound_withoutMovingMagnifier() throws RemoteException { mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, 50f, 50f); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, true); final Region outRegion = new Region(); mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, outRegion); final Rect requestedRect = outRegion.getBounds(); @@ -328,15 +347,32 @@ public class WindowMagnificationManagerTest { mWindowMagnificationManager.onRectangleOnScreenRequested(TEST_DISPLAY, requestedRect.left, requestedRect.top, requestedRect.right, requestedRect.bottom); - verify(mMockConnection.getConnection(), never()).enableWindowMagnification(eq(TEST_DISPLAY), - eq(3f), eq(500f), eq(500f), eq(0f), eq(0f), notNull()); + verify(mMockConnection.getConnection(), never()) + .moveWindowMagnifierToPosition(anyInt(), anyFloat(), anyFloat(), any()); + } + @Test + public void onRectangleOnScreenRequested_imeVisibilityDefaultInvisible_withoutMovingMagnifier() + throws RemoteException { + mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); + mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, 50f, 50f); + final Region outRegion = new Region(); + mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, outRegion); + final Rect requestedRect = outRegion.getBounds(); + requestedRect.offsetTo(requestedRect.right + 10, requestedRect.bottom + 10); + + mWindowMagnificationManager.onRectangleOnScreenRequested(TEST_DISPLAY, + requestedRect.left, requestedRect.top, requestedRect.right, requestedRect.bottom); + + verify(mMockConnection.getConnection(), never()) + .moveWindowMagnifierToPosition(anyInt(), anyFloat(), anyFloat(), any()); } @Test - public void onRectangleOnScreenRequested_trackingEnabledByDefault_movingMagnification() + public void onRectangleOnScreenRequested_trackingEnabledByDefault_movingMagnifier() throws RemoteException { mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, 50f, 50f); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, true); final Region outRegion = new Region(); mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, outRegion); final Rect requestedRect = outRegion.getBounds(); @@ -345,38 +381,56 @@ public class WindowMagnificationManagerTest { mWindowMagnificationManager.onRectangleOnScreenRequested(TEST_DISPLAY, requestedRect.left, requestedRect.top, requestedRect.right, requestedRect.bottom); - verify(mMockConnection.getConnection()).enableWindowMagnification(eq(TEST_DISPLAY), eq(3f), + verify(mMockConnection.getConnection()).moveWindowMagnifierToPosition(eq(TEST_DISPLAY), eq(requestedRect.exactCenterX()), eq(requestedRect.exactCenterY()), - eq(0f), eq(0f), notNull()); + any(IRemoteMagnificationAnimationCallback.class)); } @Test - public void onRectangleOnScreenRequested_trackingEnabledByDragAndReset_movingMagnification() + public void onRectangleOnScreenRequested_imeInvisible_withoutMovingMagnifier() throws RemoteException { - final PointF initialPoint = new PointF(50f, 50f); mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); - mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, - initialPoint.x, initialPoint.y); - mMockConnection.getConnectionCallback().onDrag(TEST_DISPLAY); - mWindowMagnificationManager.onImeWindowVisibilityChanged(true); + mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, 50f, 50f); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, true); final Region outRegion = new Region(); mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, outRegion); final Rect requestedRect = outRegion.getBounds(); requestedRect.offsetTo(requestedRect.right + 10, requestedRect.bottom + 10); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, false); mWindowMagnificationManager.onRectangleOnScreenRequested(TEST_DISPLAY, requestedRect.left, requestedRect.top, requestedRect.right, requestedRect.bottom); - verify(mMockConnection.getConnection()).enableWindowMagnification(eq(TEST_DISPLAY), - eq(3f), eq(requestedRect.exactCenterX()), eq(requestedRect.exactCenterY()), - eq(0f), eq(0f), notNull()); + verify(mMockConnection.getConnection(), never()) + .moveWindowMagnifierToPosition(anyInt(), anyFloat(), anyFloat(), any()); } @Test - public void onRectangleOnScreenRequested_followTypingIsDisabled_withoutMovingMagnification() + public void onRectangleOnScreenRequested_trackingEnabledByDragAndReset_movingMagnifier() throws RemoteException { mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, 50f, 50f); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, true); + mMockConnection.getConnectionCallback().onMove(TEST_DISPLAY); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, true); + final Region outRegion = new Region(); + mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, outRegion); + final Rect requestedRect = outRegion.getBounds(); + requestedRect.offsetTo(requestedRect.right + 10, requestedRect.bottom + 10); + + mWindowMagnificationManager.onRectangleOnScreenRequested(TEST_DISPLAY, + requestedRect.left, requestedRect.top, requestedRect.right, requestedRect.bottom); + + verify(mMockConnection.getConnection()).moveWindowMagnifierToPosition(eq(TEST_DISPLAY), + eq(requestedRect.exactCenterX()), eq(requestedRect.exactCenterY()), + any(IRemoteMagnificationAnimationCallback.class)); + } + + @Test + public void onRectangleOnScreenRequested_followTypingIsDisabled_withoutMovingMagnifier() { + mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); + mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, 50f, 50f); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, true); final Region beforeRegion = new Region(); mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, beforeRegion); final Rect requestedRect = beforeRegion.getBounds(); @@ -392,6 +446,48 @@ public class WindowMagnificationManagerTest { } @Test + public void onRectangleOnScreenRequested_trackingDisabled_withoutMovingMagnifier() { + mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); + mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, 50f, 50f); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, true); + mWindowMagnificationManager.setTrackingTypingFocusEnabled(TEST_DISPLAY, false); + final Region beforeRegion = new Region(); + mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, beforeRegion); + final Rect requestedRect = beforeRegion.getBounds(); + requestedRect.offsetTo(requestedRect.right + 10, requestedRect.bottom + 10); + + mWindowMagnificationManager.onRectangleOnScreenRequested(TEST_DISPLAY, + requestedRect.left, requestedRect.top, requestedRect.right, requestedRect.bottom); + + final Region afterRegion = new Region(); + mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, afterRegion); + assertEquals(afterRegion, beforeRegion); + } + + @Test + public void onRectangleOnScreenRequested_trackingDisabledAndEnabledMagnifier_movingMagnifier() + throws RemoteException { + mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); + mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3.0f, 50f, 50f); + mWindowMagnificationManager.onImeWindowVisibilityChanged(TEST_DISPLAY, true); + mWindowMagnificationManager.setTrackingTypingFocusEnabled(TEST_DISPLAY, false); + final Region beforeRegion = new Region(); + mWindowMagnificationManager.getMagnificationSourceBounds(TEST_DISPLAY, beforeRegion); + final Rect requestedRect = beforeRegion.getBounds(); + requestedRect.offsetTo(requestedRect.right + 10, requestedRect.bottom + 10); + mWindowMagnificationManager.disableWindowMagnification(TEST_DISPLAY, false); + // Enabling a window magnifier again will turn on the tracking typing focus functionality. + mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, NaN, NaN, NaN); + + mWindowMagnificationManager.onRectangleOnScreenRequested(TEST_DISPLAY, + requestedRect.left, requestedRect.top, requestedRect.right, requestedRect.bottom); + + verify(mMockConnection.getConnection()).moveWindowMagnifierToPosition(eq(TEST_DISPLAY), + eq(requestedRect.exactCenterX()), eq(requestedRect.exactCenterY()), + any(IRemoteMagnificationAnimationCallback.class)); + } + + @Test public void moveWindowMagnifier_enabled_invokeConnectionMethod() throws RemoteException { mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 2f, NaN, NaN); @@ -464,7 +560,7 @@ public class WindowMagnificationManagerTest { public void requestConnectionToNull_disableAllMagnifiersAndRequestWindowMagnificationConnection() throws RemoteException { - mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); + assertTrue(mWindowMagnificationManager.requestConnection(true)); mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3f, NaN, NaN); assertTrue(mWindowMagnificationManager.requestConnection(false)); @@ -499,7 +595,7 @@ public class WindowMagnificationManagerTest { @Test public void requestConnectionToNull_expectedGetterResults() { - mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); + mWindowMagnificationManager.requestConnection(true); mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3f, 1, 1); mWindowMagnificationManager.requestConnection(false); @@ -513,6 +609,20 @@ public class WindowMagnificationManagerTest { } @Test + public void enableWindowMagnification_connecting_invokeConnectionMethodAfterConnected() + throws RemoteException { + stubSetConnection(true); + mWindowMagnificationManager.requestConnection(true); + + assertTrue(mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3f, 1, 1)); + + // Invoke enableWindowMagnification if the connection is connected. + verify(mMockConnection.getConnection()).enableWindowMagnification( + eq(TEST_DISPLAY), eq(3f), + eq(1f), eq(1f), eq(0f), eq(0f), notNull()); + } + + @Test public void resetAllMagnification_enabledBySameId_windowMagnifiersDisabled() { mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 3f, 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 b255a35c512e..25cf8a86baad 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -219,7 +219,7 @@ public class AuthSessionTest { public void testMultiAuth_singleSensor_fingerprintSensorStartsAfterDialogAnimationCompletes() throws Exception { setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); - testMultiAuth_fingerprintSensorStartsAfter(false /* fingerprintStartsAfterDelay */); + testMultiAuth_fingerprintSensorStartsAfterUINotifies(); } @Test @@ -227,10 +227,10 @@ public class AuthSessionTest { throws Exception { setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); setupFace(1 /* id */, false, mock(IBiometricAuthenticator.class)); - testMultiAuth_fingerprintSensorStartsAfter(true /* fingerprintStartsAfterDelay */); + testMultiAuth_fingerprintSensorStartsAfterUINotifies(); } - public void testMultiAuth_fingerprintSensorStartsAfter(boolean fingerprintStartsAfterDelay) + public void testMultiAuth_fingerprintSensorStartsAfterUINotifies() throws Exception { final long operationId = 123; final int userId = 10; @@ -274,12 +274,6 @@ public class AuthSessionTest { // Notify AuthSession that the UI is shown. Then, fingerprint sensor should be started. session.onDialogAnimatedIn(); - if (fingerprintStartsAfterDelay) { - assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); - assertEquals(BiometricSensor.STATE_COOKIE_RETURNED, - session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState()); - session.onStartFingerprint(); - } assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState()); assertEquals(BiometricSensor.STATE_AUTHENTICATING, session.mPreAuthInfo.eligibleSensors.get(fingerprintSensorId).getSensorState()); 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 b94b6908f030..2ad5eaeb9aaf 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -16,6 +16,7 @@ package com.android.server.biometrics; +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; @@ -85,14 +86,11 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Random; -import java.util.concurrent.atomic.AtomicLong; @Presubmit @SmallTest public class BiometricServiceTest { - private static final String TAG = "BiometricServiceTest"; - private static final String TEST_PACKAGE_NAME = "test_package"; private static final long TEST_REQUEST_ID = 44; @@ -153,7 +151,7 @@ public class BiometricServiceTest { .thenReturn(mock(BiometricStrengthController.class)); when(mInjector.getTrustManager()).thenReturn(mTrustManager); when(mInjector.getDevicePolicyManager(any())).thenReturn(mDevicePolicyManager); - when(mInjector.getRequestGenerator()).thenReturn(new AtomicLong(TEST_REQUEST_ID - 1)); + when(mInjector.getRequestGenerator()).thenReturn(() -> TEST_REQUEST_ID); when(mResources.getString(R.string.biometric_error_hw_unavailable)) .thenReturn(ERROR_HW_UNAVAILABLE); @@ -178,22 +176,22 @@ public class BiometricServiceTest { invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, true /* requireConfirmation */, null /* authenticators */); waitForIdle(); - verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mCurrentAuthSession), + verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mAuthSession), anyInt()); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FACE, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, 0 /* vendorCode */); waitForIdle(); - assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState()); - mBiometricService.mCurrentAuthSession.binderDied(); + mBiometricService.mAuthSession.binderDied(); waitForIdle(); - assertNull(mBiometricService.mCurrentAuthSession); + assertNull(mBiometricService.mAuthSession); verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); } @@ -205,31 +203,31 @@ public class BiometricServiceTest { invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, true /* requireConfirmation */, null /* authenticators */); waitForIdle(); - verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mCurrentAuthSession), + verify(mReceiver1.asBinder()).linkToDeath(eq(mBiometricService.mAuthSession), anyInt()); - assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); - mBiometricService.mCurrentAuthSession.binderDied(); + assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState()); + mBiometricService.mAuthSession.binderDied(); waitForIdle(); - assertNotNull(mBiometricService.mCurrentAuthSession); + assertNotNull(mBiometricService.mAuthSession); verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog(); assertEquals(STATE_CLIENT_DIED_CANCELLING, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); - verify(mBiometricService.mCurrentAuthSession.mPreAuthInfo.eligibleSensors.get(0).impl) + verify(mBiometricService.mAuthSession.mPreAuthInfo.eligibleSensors.get(0).impl) .cancelAuthenticationFromService(any(), any(), anyLong()); // Simulate ERROR_CANCELED received from HAL - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FACE, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_CANCELED, 0 /* vendorCode */); waitForIdle(); verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); - assertNull(mBiometricService.mCurrentAuthSession); + assertNull(mBiometricService.mAuthSession); } @Test @@ -265,12 +263,12 @@ public class BiometricServiceTest { Authenticators.DEVICE_CREDENTIAL); waitForIdle(); - assertNotNull(mBiometricService.mCurrentAuthSession); + assertNotNull(mBiometricService.mAuthSession); assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); // StatusBar showBiometricDialog invoked verify(mBiometricService.mStatusBarService).showAuthenticationDialog( - eq(mBiometricService.mCurrentAuthSession.mPromptInfo), + eq(mBiometricService.mAuthSession.mPromptInfo), any(IBiometricSysuiReceiver.class), AdditionalMatchers.aryEq(new int[0]) /* sensorIds */, eq(true) /* credentialAllowed */, @@ -304,21 +302,21 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, mInjector); mBiometricService.onStart(); mBiometricService.mImpl.registerAuthenticator(0 /* id */, - BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); waitForIdle(); verify(mReceiver1).onError( - eq(BiometricAuthenticator.TYPE_FINGERPRINT), + eq(TYPE_FINGERPRINT), eq(BiometricConstants.BIOMETRIC_ERROR_NO_BIOMETRICS), eq(0 /* vendorCode */)); } @Test public void testAuthenticate_notStrongEnough_returnsHardwareNotPresent() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, Authenticators.BIOMETRIC_STRONG); @@ -335,7 +333,7 @@ public class BiometricServiceTest { // is able to proceed. final int[] modalities = new int[] { - BiometricAuthenticator.TYPE_FINGERPRINT, + TYPE_FINGERPRINT, BiometricAuthenticator.TYPE_FACE, }; @@ -356,7 +354,7 @@ public class BiometricServiceTest { // StatusBar showBiometricDialog invoked with face, which was set up to be STRONG verify(mBiometricService.mStatusBarService).showAuthenticationDialog( - eq(mBiometricService.mCurrentAuthSession.mPromptInfo), + eq(mBiometricService.mAuthSession.mPromptInfo), any(IBiometricSysuiReceiver.class), AdditionalMatchers.aryEq(new int[] {SENSOR_ID_FACE}), eq(false) /* credentialAllowed */, @@ -377,14 +375,14 @@ public class BiometricServiceTest { mBiometricService = new BiometricService(mContext, mInjector); mBiometricService.onStart(); mBiometricService.mImpl.registerAuthenticator(0 /* id */, - BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); waitForIdle(); verify(mReceiver1).onError( - eq(BiometricAuthenticator.TYPE_FINGERPRINT), + eq(TYPE_FINGERPRINT), eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(0 /* vendorCode */)); } @@ -415,13 +413,13 @@ public class BiometricServiceTest { waitForIdle(); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); final byte[] HAT = generateRandomHAT(); - mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded( + mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded( SENSOR_ID_FACE, HAT); waitForIdle(); // Confirmation is required assertEquals(STATE_AUTH_PENDING_CONFIRM, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); // Enrolled, not disabled in settings, user doesn't require confirmation in settings resetReceivers(); @@ -431,25 +429,25 @@ public class BiometricServiceTest { invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); waitForIdle(); - mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded( + mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded( SENSOR_ID_FACE, HAT); waitForIdle(); // Confirmation not required, waiting for dialog to dismiss assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); } @Test public void testAuthenticate_happyPathWithoutConfirmation_strongBiometric() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); testAuthenticate_happyPathWithoutConfirmation(true /* isStrongBiometric */); } @Test public void testAuthenticate_happyPathWithoutConfirmation_weakBiometric() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK); testAuthenticate_happyPathWithoutConfirmation(false /* isStrongBiometric */); } @@ -461,7 +459,7 @@ public class BiometricServiceTest { waitForIdle(); // Creates a pending auth session with the correct initial states - assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState()); // Invokes <Modality>Service#prepareForAuthentication ArgumentCaptor<Integer> cookieCaptor = ArgumentCaptor.forClass(Integer.class); @@ -477,19 +475,19 @@ public class BiometricServiceTest { cookieCaptor.capture() /* cookie */, anyBoolean() /* allowBackgroundAuthentication */); - // onReadyForAuthentication, mCurrentAuthSession state OK - mBiometricService.mImpl.onReadyForAuthentication(cookieCaptor.getValue()); + // onReadyForAuthentication, mAuthSession state OK + mBiometricService.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookieCaptor.getValue()); waitForIdle(); - assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState()); // startPreparedClient invoked - mBiometricService.mCurrentAuthSession.onDialogAnimatedIn(); + mBiometricService.mAuthSession.onDialogAnimatedIn(); verify(mBiometricService.mSensors.get(0).impl) .startPreparedClient(cookieCaptor.getValue()); // StatusBar showBiometricDialog invoked verify(mBiometricService.mStatusBarService).showAuthenticationDialog( - eq(mBiometricService.mCurrentAuthSession.mPromptInfo), + eq(mBiometricService.mAuthSession.mPromptInfo), any(IBiometricSysuiReceiver.class), any(), eq(false) /* credentialAllowed */, @@ -502,18 +500,18 @@ public class BiometricServiceTest { // Hardware authenticated final byte[] HAT = generateRandomHAT(); - mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded( + mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded( SENSOR_ID_FINGERPRINT, HAT); waitForIdle(); // Waiting for SystemUI to send dismissed callback assertEquals(STATE_AUTHENTICATED_PENDING_SYSUI, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); // Notify SystemUI hardware authenticated - verify(mBiometricService.mStatusBarService).onBiometricAuthenticated(); + verify(mBiometricService.mStatusBarService).onBiometricAuthenticated(TYPE_FINGERPRINT); // SystemUI sends callback with dismissed reason - mBiometricService.mSysuiReceiver.onDialogDismissed( + mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed( BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED, null /* credentialAttestation */); waitForIdle(); @@ -527,7 +525,7 @@ public class BiometricServiceTest { verify(mReceiver1).onAuthenticationSucceeded( BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC); // Current session becomes null - assertNull(mBiometricService.mCurrentAuthSession); + assertNull(mBiometricService.mAuthSession); } @Test @@ -542,11 +540,11 @@ public class BiometricServiceTest { waitForIdle(); assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); assertEquals(Authenticators.DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators()); + mBiometricService.mAuthSession.mPromptInfo.getAuthenticators()); verify(mBiometricService.mStatusBarService).showAuthenticationDialog( - eq(mBiometricService.mCurrentAuthSession.mPromptInfo), + eq(mBiometricService.mAuthSession.mPromptInfo), any(IBiometricSysuiReceiver.class), AdditionalMatchers.aryEq(new int[0]) /* sensorIds */, eq(true) /* credentialAllowed */, @@ -578,16 +576,16 @@ public class BiometricServiceTest { // Test authentication succeeded goes to PENDING_CONFIRMATION and that the HAT is not // sent to KeyStore yet final byte[] HAT = generateRandomHAT(); - mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded( + mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded( SENSOR_ID_FACE, HAT); waitForIdle(); // Waiting for SystemUI to send confirmation callback - assertEquals(STATE_AUTH_PENDING_CONFIRM, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_PENDING_CONFIRM, mBiometricService.mAuthSession.getState()); verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class)); // SystemUI sends confirm, HAT is sent to keystore and client is notified. - mBiometricService.mSysuiReceiver.onDialogDismissed( + mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed( BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED, null /* credentialAttestation */); waitForIdle(); @@ -624,33 +622,34 @@ public class BiometricServiceTest { invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - mBiometricService.mBiometricSensorReceiver.onAuthenticationFailed(SENSOR_ID_FACE); + mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationFailed(SENSOR_ID_FACE); waitForIdle(); verify(mBiometricService.mStatusBarService).onBiometricError( - eq(BiometricAuthenticator.TYPE_NONE), + eq(BiometricAuthenticator.TYPE_FACE), eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED), eq(0 /* vendorCode */)); verify(mReceiver1).onAuthenticationFailed(); - assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState()); } @Test public void testRejectFingerprint_whenAuthenticating_notifiesAndKeepsAuthenticating() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - mBiometricService.mBiometricSensorReceiver.onAuthenticationFailed(SENSOR_ID_FINGERPRINT); + mBiometricService.mAuthSession.mSensorReceiver + .onAuthenticationFailed(SENSOR_ID_FINGERPRINT); waitForIdle(); verify(mBiometricService.mStatusBarService).onBiometricError( - eq(BiometricAuthenticator.TYPE_NONE), + eq(TYPE_FINGERPRINT), eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED), eq(0 /* vendorCode */)); verify(mReceiver1).onAuthenticationFailed(); - assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState()); } @Test @@ -678,14 +677,14 @@ public class BiometricServiceTest { invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FACE, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, 0 /* vendorCode */); waitForIdle(); - assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState()); verify(mBiometricService.mStatusBarService).onBiometricError( eq(BiometricAuthenticator.TYPE_FACE), eq(BiometricConstants.BIOMETRIC_ERROR_TIMEOUT), @@ -694,15 +693,15 @@ public class BiometricServiceTest { verify(mReceiver1, never()).onAuthenticationFailed(); // No auth session. Pressing try again will create one. - assertEquals(STATE_AUTH_PAUSED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState()); // Pressing "Try again" on SystemUI - mBiometricService.mSysuiReceiver.onTryAgainPressed(); + mBiometricService.mAuthSession.mSysuiReceiver.onTryAgainPressed(); waitForIdle(); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); // AuthSession is now resuming - assertEquals(STATE_AUTH_PAUSED_RESUMING, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_PAUSED_RESUMING, mBiometricService.mAuthSession.getState()); // Test resuming when hardware becomes ready. SystemUI should not be requested to // show another dialog since it's already showing. @@ -728,14 +727,14 @@ public class BiometricServiceTest { invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FACE, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, 0 /* vendorCode */); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FACE, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_CANCELED, 0 /* vendorCode */); waitForIdle(); @@ -748,7 +747,7 @@ public class BiometricServiceTest { // Dialog is hidden immediately verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(); // Auth session is over - assertNull(mBiometricService.mCurrentAuthSession); + assertNull(mBiometricService.mAuthSession); } @Test @@ -757,61 +756,61 @@ public class BiometricServiceTest { // For errors that show in SystemUI, BiometricService stays in STATE_ERROR_PENDING_SYSUI // until SystemUI notifies us that the dialog is dismissed at which point the current // session is done. - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FINGERPRINT, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS, 0 /* vendorCode */); waitForIdle(); // Sends error to SystemUI and does not notify client yet - assertEquals(STATE_ERROR_PENDING_SYSUI, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_ERROR_PENDING_SYSUI, mBiometricService.mAuthSession.getState()); verify(mBiometricService.mStatusBarService).onBiometricError( - eq(BiometricAuthenticator.TYPE_FINGERPRINT), + eq(TYPE_FINGERPRINT), eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), eq(0 /* vendorCode */)); verify(mBiometricService.mStatusBarService, never()).hideAuthenticationDialog(); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); // SystemUI animation completed, client is notified, auth session is over - mBiometricService.mSysuiReceiver.onDialogDismissed( + mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed( BiometricPrompt.DISMISSED_REASON_ERROR, null /* credentialAttestation */); waitForIdle(); verify(mReceiver1).onError( - eq(BiometricAuthenticator.TYPE_FINGERPRINT), + eq(TYPE_FINGERPRINT), eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), eq(0 /* vendorCode */)); - assertNull(mBiometricService.mCurrentAuthSession); + assertNull(mBiometricService.mAuthSession); } @Test public void testErrorFromHal_whilePreparingAuthentication_credentialAllowed() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK); waitForIdle(); - assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState()); - mBiometricService.mBiometricSensorReceiver.onError( + assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState()); + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FINGERPRINT, - getCookieForPendingSession(mBiometricService.mCurrentAuthSession), + getCookieForPendingSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, 0 /* vendorCode */); waitForIdle(); // We should be showing device credential now - assertNotNull(mBiometricService.mCurrentAuthSession); + assertNotNull(mBiometricService.mAuthSession); assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); assertEquals(Authenticators.DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators()); + mBiometricService.mAuthSession.mPromptInfo.getAuthenticators()); verify(mBiometricService.mStatusBarService).showAuthenticationDialog( - eq(mBiometricService.mCurrentAuthSession.mPromptInfo), + eq(mBiometricService.mAuthSession.mPromptInfo), any(IBiometricSysuiReceiver.class), AdditionalMatchers.aryEq(new int[0]) /* sensorIds */, eq(true) /* credentialAllowed */, @@ -826,23 +825,23 @@ public class BiometricServiceTest { @Test public void testErrorFromHal_whilePreparingAuthentication_credentialNotAllowed() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); waitForIdle(); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FINGERPRINT, - getCookieForPendingSession(mBiometricService.mCurrentAuthSession), + getCookieForPendingSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, 0 /* vendorCode */); waitForIdle(); // Error is sent to client - verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT), + verify(mReceiver1).onError(eq(TYPE_FINGERPRINT), eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT), eq(0) /* vendorCode */); - assertNull(mBiometricService.mCurrentAuthSession); + assertNull(mBiometricService.mAuthSession); } @Test @@ -861,7 +860,7 @@ public class BiometricServiceTest { private void testBiometricAuth_whenLockout(@LockoutTracker.LockoutMode int lockoutMode, int biometricPromptError) throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt())) .thenReturn(lockoutMode); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, @@ -869,16 +868,15 @@ public class BiometricServiceTest { waitForIdle(); // Modality and error are sent - verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT), + verify(mReceiver1).onError(eq(TYPE_FINGERPRINT), eq(biometricPromptError), eq(0) /* vendorCode */); } @Test public void testBiometricOrCredentialAuth_whenBiometricLockout_showsCredential() throws Exception { - when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) - .thenReturn(true); - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt())) .thenReturn(LockoutTracker.LOCKOUT_PERMANENT); invokeAuthenticate(mBiometricService.mImpl, mReceiver1, @@ -887,13 +885,13 @@ public class BiometricServiceTest { waitForIdle(); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); - assertNotNull(mBiometricService.mCurrentAuthSession); + assertNotNull(mBiometricService.mAuthSession); assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); assertEquals(Authenticators.DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.mPromptInfo.getAuthenticators()); + mBiometricService.mAuthSession.mPromptInfo.getAuthenticators()); verify(mBiometricService.mStatusBarService).showAuthenticationDialog( - eq(mBiometricService.mCurrentAuthSession.mPromptInfo), + eq(mBiometricService.mAuthSession.mPromptInfo), any(IBiometricSysuiReceiver.class), AdditionalMatchers.aryEq(new int[0]) /* sensorIds */, eq(true) /* credentialAllowed */, @@ -959,73 +957,73 @@ public class BiometricServiceTest { @Test public void testErrorFromHal_whileShowingDeviceCredential_doesntNotifySystemUI() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK); - mBiometricService.mSysuiReceiver.onDeviceCredentialPressed(); + mBiometricService.mAuthSession.mSysuiReceiver.onDeviceCredentialPressed(); waitForIdle(); assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FINGERPRINT, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_CANCELED, 0 /* vendorCode */); waitForIdle(); assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); verify(mReceiver1, never()).onError(anyInt(), anyInt(), anyInt()); } @Test public void testLockout_whileAuthenticating_credentialAllowed() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK); - assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState()); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FINGERPRINT, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, 0 /* vendorCode */); waitForIdle(); assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); verify(mBiometricService.mStatusBarService).onBiometricError( - eq(BiometricAuthenticator.TYPE_FINGERPRINT), + eq(TYPE_FINGERPRINT), eq(BiometricConstants.BIOMETRIC_ERROR_LOCKOUT), eq(0 /* vendorCode */)); } @Test public void testLockout_whenAuthenticating_credentialNotAllowed() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState()); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FINGERPRINT, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS, 0 /* vendorCode */); waitForIdle(); assertEquals(STATE_ERROR_PENDING_SYSUI, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); verify(mBiometricService.mStatusBarService).onBiometricError( - eq(BiometricAuthenticator.TYPE_FINGERPRINT), + eq(TYPE_FINGERPRINT), eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_PROCESS), eq(0 /* vendorCode */)); } @@ -1033,20 +1031,20 @@ public class BiometricServiceTest { @Test public void testDismissedReasonUserCancel_whileAuthenticating_cancelsHalAuthentication() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - mBiometricService.mSysuiReceiver.onDialogDismissed( + mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed( BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */); waitForIdle(); verify(mReceiver1).onError( - eq(BiometricAuthenticator.TYPE_FINGERPRINT), + eq(TYPE_FINGERPRINT), eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), eq(0 /* vendorCode */)); verify(mBiometricService.mSensors.get(0).impl).cancelAuthenticationFromService( any(), any(), anyLong()); - assertNull(mBiometricService.mCurrentAuthSession); + assertNull(mBiometricService.mAuthSession); } @Test @@ -1055,12 +1053,12 @@ public class BiometricServiceTest { invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FACE, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, 0 /* vendorCode */); - mBiometricService.mSysuiReceiver.onDialogDismissed( + mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed( BiometricPrompt.DISMISSED_REASON_NEGATIVE, null /* credentialAttestation */); waitForIdle(); @@ -1069,18 +1067,17 @@ public class BiometricServiceTest { } @Test - public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws - Exception { + public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws Exception { setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FACE, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, 0 /* vendorCode */); - mBiometricService.mSysuiReceiver.onDialogDismissed( + mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed( BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */); waitForIdle(); @@ -1094,10 +1091,10 @@ public class BiometricServiceTest { invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, true /* requireConfirmation */, null /* authenticators */); - mBiometricService.mBiometricSensorReceiver.onAuthenticationSucceeded( + mBiometricService.mAuthSession.mSensorReceiver.onAuthenticationSucceeded( SENSOR_ID_FACE, new byte[69] /* HAT */); - mBiometricService.mSysuiReceiver.onDialogDismissed( + mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed( BiometricPrompt.DISMISSED_REASON_USER_CANCEL, null /* credentialAttestation */); waitForIdle(); @@ -1108,19 +1105,19 @@ public class BiometricServiceTest { eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), eq(0 /* vendorCode */)); verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class)); - assertNull(mBiometricService.mCurrentAuthSession); + assertNull(mBiometricService.mAuthSession); } @Test public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception { when(mContext.getResources().getString(anyInt())).thenReturn("test string"); - final int modality = BiometricAuthenticator.TYPE_FINGERPRINT; + final int modality = TYPE_FINGERPRINT; setupAuthForOnly(modality, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - mBiometricService.mBiometricSensorReceiver.onAcquired( + mBiometricService.mAuthSession.mSensorReceiver.onAcquired( SENSOR_ID_FINGERPRINT, FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY, 0 /* vendorCode */); @@ -1130,29 +1127,29 @@ public class BiometricServiceTest { // string is retrieved for now, but it's also very unlikely to break anyway. verify(mBiometricService.mStatusBarService) .onBiometricHelp(eq(modality), anyString()); - assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState()); } @Test public void testCancel_whenAuthenticating() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); - mBiometricService.mImpl.cancelAuthentication(mBiometricService.mCurrentAuthSession.mToken, + mBiometricService.mImpl.cancelAuthentication(mBiometricService.mAuthSession.mToken, TEST_PACKAGE_NAME, TEST_REQUEST_ID); waitForIdle(); // Pretend that the HAL has responded to cancel with ERROR_CANCELED - mBiometricService.mBiometricSensorReceiver.onError( + mBiometricService.mAuthSession.mSensorReceiver.onError( SENSOR_ID_FINGERPRINT, - getCookieForCurrentSession(mBiometricService.mCurrentAuthSession), + getCookieForCurrentSession(mBiometricService.mAuthSession), BiometricConstants.BIOMETRIC_ERROR_CANCELED, 0 /* vendorCode */); waitForIdle(); // Hides system dialog and invokes the onError callback - verify(mReceiver1).onError(eq(BiometricAuthenticator.TYPE_FINGERPRINT), + verify(mReceiver1).onError(eq(TYPE_FINGERPRINT), eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), eq(0 /* vendorCode */)); verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(); @@ -1161,7 +1158,7 @@ public class BiometricServiceTest { @Test public void testCanAuthenticate_whenDeviceHasRequestedBiometricStrength() throws Exception { // When only biometric is requested, and sensor is strong enough - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); assertEquals(BiometricManager.BIOMETRIC_SUCCESS, invokeCanAuthenticate(mBiometricService, Authenticators.BIOMETRIC_STRONG)); @@ -1170,7 +1167,7 @@ public class BiometricServiceTest { @Test public void testCanAuthenticate_whenDeviceDoesNotHaveRequestedBiometricStrength() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_WEAK); // When only biometric is requested, and sensor is not strong enough when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) @@ -1208,9 +1205,8 @@ public class BiometricServiceTest { @Test public void testCanAuthenticate_whenNoBiometricsEnrolled() throws Exception { // With credential set up, test the following. - when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) - .thenReturn(true); - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, + when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, false /* enrolled */); // When only biometric is requested @@ -1277,7 +1273,7 @@ public class BiometricServiceTest { private void testCanAuthenticate_whenLockedOut(@LockoutTracker.LockoutMode int lockoutMode) throws Exception { // When only biometric is requested, and sensor is strong enough - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt())) .thenReturn(lockoutMode); @@ -1311,7 +1307,7 @@ public class BiometricServiceTest { for (int i = 0; i < testCases.length; i++) { final BiometricSensor sensor = new BiometricSensor(mContext, 0 /* id */, - BiometricAuthenticator.TYPE_FINGERPRINT, + TYPE_FINGERPRINT, testCases[i][0], mock(IBiometricAuthenticator.class)) { @Override @@ -1341,7 +1337,7 @@ public class BiometricServiceTest { .thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); mBiometricService.mImpl.registerAuthenticator(0 /* testId */, - BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); verify(mBiometricService.mBiometricStrengthController).updateStrengths(); @@ -1360,7 +1356,7 @@ public class BiometricServiceTest { .thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); mBiometricService.mImpl.registerAuthenticator(testId /* id */, - BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, + TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG, mFingerprintAuthenticator); // Downgrade the authenticator @@ -1378,7 +1374,7 @@ public class BiometricServiceTest { false /* requireConfirmation */, authenticators); waitForIdle(); verify(mReceiver1).onError( - eq(BiometricAuthenticator.TYPE_FINGERPRINT), + eq(TYPE_FINGERPRINT), eq(BiometricPrompt.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED), eq(0) /* vendorCode */); @@ -1392,7 +1388,7 @@ public class BiometricServiceTest { authenticators); waitForIdle(); verify(mBiometricService.mStatusBarService).showAuthenticationDialog( - eq(mBiometricService.mCurrentAuthSession.mPromptInfo), + eq(mBiometricService.mAuthSession.mPromptInfo), any(IBiometricSysuiReceiver.class), AdditionalMatchers.aryEq(new int[] {testId}), eq(false) /* credentialAllowed */, @@ -1414,9 +1410,9 @@ public class BiometricServiceTest { false /* requireConfirmation */, authenticators); waitForIdle(); - assertTrue(Utils.isCredentialRequested(mBiometricService.mCurrentAuthSession.mPromptInfo)); + assertTrue(Utils.isCredentialRequested(mBiometricService.mAuthSession.mPromptInfo)); verify(mBiometricService.mStatusBarService).showAuthenticationDialog( - eq(mBiometricService.mCurrentAuthSession.mPromptInfo), + eq(mBiometricService.mAuthSession.mPromptInfo), any(IBiometricSysuiReceiver.class), AdditionalMatchers.aryEq(new int[0]) /* sensorIds */, eq(true) /* credentialAllowed */, @@ -1442,7 +1438,7 @@ public class BiometricServiceTest { false /* requireConfirmation */, authenticators); waitForIdle(); verify(mBiometricService.mStatusBarService).showAuthenticationDialog( - eq(mBiometricService.mCurrentAuthSession.mPromptInfo), + eq(mBiometricService.mAuthSession.mPromptInfo), any(IBiometricSysuiReceiver.class), AdditionalMatchers.aryEq(new int[] {testId}) /* sensorIds */, eq(false) /* credentialAllowed */, @@ -1495,29 +1491,29 @@ public class BiometricServiceTest { @Test public void testWorkAuthentication_fingerprintWorksIfNotDisabledByDevicePolicyManager() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); when(mDevicePolicyManager .getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */)) .thenReturn(~DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT); invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1, Authenticators.BIOMETRIC_STRONG); waitForIdle(); - assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState()); startPendingAuthSession(mBiometricService); waitForIdle(); - assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState()); } @Test public void testAuthentication_normalAppIgnoresDevicePolicy() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); when(mDevicePolicyManager .getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */)) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, Authenticators.BIOMETRIC_STRONG); waitForIdle(); - assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState()); } @Test @@ -1530,18 +1526,17 @@ public class BiometricServiceTest { invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver1, Authenticators.BIOMETRIC_STRONG); waitForIdle(); - assertEquals(STATE_AUTH_CALLED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_CALLED, mBiometricService.mAuthSession.getState()); startPendingAuthSession(mBiometricService); waitForIdle(); - assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); + assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState()); } @Test public void testWorkAuthentication_fingerprintFailsIfDisabledByDevicePolicyManager() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); - when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) - .thenReturn(true); + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true); when(mDevicePolicyManager .getKeyguardDisabledFeatures(any() /* admin */, anyInt() /* userHandle */)) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT); @@ -1555,9 +1550,9 @@ public class BiometricServiceTest { invokeAuthenticateForWorkApp(mBiometricService.mImpl, mReceiver2, Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL); waitForIdle(); - assertNotNull(mBiometricService.mCurrentAuthSession); + assertNotNull(mBiometricService.mAuthSession); assertEquals(STATE_SHOWING_DEVICE_CREDENTIAL, - mBiometricService.mCurrentAuthSession.getState()); + mBiometricService.mAuthSession.getState()); verify(mReceiver2, never()).onError(anyInt(), anyInt(), anyInt()); } @@ -1580,7 +1575,7 @@ public class BiometricServiceTest { when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); - if ((modality & BiometricAuthenticator.TYPE_FINGERPRINT) != 0) { + if ((modality & TYPE_FINGERPRINT) != 0) { when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())) .thenReturn(enrolled); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); @@ -1614,7 +1609,7 @@ public class BiometricServiceTest { final int modality = modalities[i]; final int strength = strengths[i]; - if ((modality & BiometricAuthenticator.TYPE_FINGERPRINT) != 0) { + if ((modality & TYPE_FINGERPRINT) != 0) { when(mFingerprintAuthenticator.hasEnrolledTemplates(anyInt(), any())) .thenReturn(true); when(mFingerprintAuthenticator.isHardwareDetected(any())).thenReturn(true); @@ -1654,8 +1649,9 @@ public class BiometricServiceTest { startPendingAuthSession(mBiometricService); waitForIdle(); - assertNotNull(mBiometricService.mCurrentAuthSession); - assertEquals(STATE_AUTH_STARTED, mBiometricService.mCurrentAuthSession.getState()); + assertNotNull(mBiometricService.mAuthSession); + assertEquals(TEST_REQUEST_ID, mBiometricService.mAuthSession.getRequestId()); + assertEquals(STATE_AUTH_STARTED, mBiometricService.mAuthSession.getState()); return requestId; } @@ -1663,14 +1659,14 @@ public class BiometricServiceTest { private static void startPendingAuthSession(BiometricService service) throws Exception { // Get the cookie so we can pretend the hardware is ready to authenticate // Currently we only support single modality per auth - final PreAuthInfo preAuthInfo = service.mCurrentAuthSession.mPreAuthInfo; + final PreAuthInfo preAuthInfo = service.mAuthSession.mPreAuthInfo; assertEquals(preAuthInfo.eligibleSensors.size(), 1); assertEquals(preAuthInfo.numSensorsWaitingForCookie(), 1); final int cookie = preAuthInfo.eligibleSensors.get(0).getCookie(); assertNotEquals(cookie, 0); - service.mImpl.onReadyForAuthentication(cookie); + service.mImpl.onReadyForAuthentication(TEST_REQUEST_ID, cookie); } private static long invokeAuthenticate(IBiometricService.Stub service, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java index bfb0be760f85..f40b31a0bc0d 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/CoexCoordinatorTest.java @@ -29,12 +29,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; -import android.content.Context; import android.hardware.biometrics.BiometricConstants; -import android.os.Handler; -import android.os.Looper; import android.platform.test.annotations.Presubmit; import androidx.test.InstrumentationRegistry; @@ -43,9 +39,11 @@ import androidx.test.filters.SmallTest; import com.android.server.biometrics.sensors.fingerprint.Udfps; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.LinkedList; @@ -53,39 +51,38 @@ import java.util.LinkedList; @SmallTest public class CoexCoordinatorTest { - private static final String TAG = "CoexCoordinatorTest"; + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); - private CoexCoordinator mCoexCoordinator; - private Handler mHandler; - - @Mock - private Context mContext; @Mock private CoexCoordinator.Callback mCallback; @Mock private CoexCoordinator.ErrorCallback mErrorCallback; + @Mock + private AuthenticationClient mFaceClient; + @Mock + private AuthenticationClient mFingerprintClient; + @Mock(extraInterfaces = {Udfps.class}) + private AuthenticationClient mUdfpsClient; + + private CoexCoordinator mCoexCoordinator; @Before public void setUp() { - MockitoAnnotations.initMocks(this); - - mHandler = new Handler(Looper.getMainLooper()); - mCoexCoordinator = CoexCoordinator.getInstance(); mCoexCoordinator.setAdvancedLogicEnabled(true); mCoexCoordinator.setFaceHapticDisabledWhenNonBypass(true); + mCoexCoordinator.reset(); } @Test public void testBiometricPrompt_authSuccess() { - mCoexCoordinator.reset(); - - AuthenticationClient<?> client = mock(AuthenticationClient.class); - when(client.isBiometricPrompt()).thenReturn(true); + when(mFaceClient.isBiometricPrompt()).thenReturn(true); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); - mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback); + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, + mFaceClient, mCallback); verify(mCallback).sendHapticFeedback(); verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); verify(mCallback).handleLifecycleAfterAuth(); @@ -93,15 +90,12 @@ public class CoexCoordinatorTest { @Test public void testBiometricPrompt_authReject_whenNotLockedOut() { - mCoexCoordinator.reset(); - - AuthenticationClient<?> client = mock(AuthenticationClient.class); - when(client.isBiometricPrompt()).thenReturn(true); + when(mFaceClient.isBiometricPrompt()).thenReturn(true); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, - client, LockoutTracker.LOCKOUT_NONE, mCallback); + mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback); verify(mCallback).sendHapticFeedback(); verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); verify(mCallback).handleLifecycleAfterAuth(); @@ -109,30 +103,97 @@ public class CoexCoordinatorTest { @Test public void testBiometricPrompt_authReject_whenLockedOut() { - mCoexCoordinator.reset(); + when(mFaceClient.isBiometricPrompt()).thenReturn(true); - AuthenticationClient<?> client = mock(AuthenticationClient.class); - when(client.isBiometricPrompt()).thenReturn(true); - - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, - client, LockoutTracker.LOCKOUT_TIMED, mCallback); + mFaceClient, LockoutTracker.LOCKOUT_TIMED, mCallback); verify(mCallback).sendHapticFeedback(); verify(mCallback, never()).sendAuthenticationResult(anyBoolean()); verify(mCallback).handleLifecycleAfterAuth(); } @Test - public void testKeyguard_faceAuthOnly_success() { - mCoexCoordinator.reset(); + public void testBiometricPrompt_coex_success() { + testBiometricPrompt_coex_success(false /* twice */); + } + + @Test + public void testBiometricPrompt_coex_successWithoutDouble() { + testBiometricPrompt_coex_success(true /* twice */); + } + + private void testBiometricPrompt_coex_success(boolean twice) { + initFaceAndFingerprintForBiometricPrompt(); + when(mFaceClient.wasAuthSuccessful()).thenReturn(true); + when(mUdfpsClient.wasAuthSuccessful()).thenReturn(twice, true); + + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient); + + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, + mFaceClient, mCallback); + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, + mUdfpsClient, mCallback); + + if (twice) { + verify(mCallback, never()).sendHapticFeedback(); + } else { + verify(mCallback).sendHapticFeedback(); + } + } + + @Test + public void testBiometricPrompt_coex_reject() { + initFaceAndFingerprintForBiometricPrompt(); + + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient); + + mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, + mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback); + + verify(mCallback, never()).sendHapticFeedback(); + + mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, + mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback); + + verify(mCallback).sendHapticFeedback(); + } + + @Test + public void testBiometricPrompt_coex_errorNoHaptics() { + initFaceAndFingerprintForBiometricPrompt(); + + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient); + + mCoexCoordinator.onAuthenticationError(mFaceClient, + BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback); + mCoexCoordinator.onAuthenticationError(mUdfpsClient, + BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback); + + verify(mErrorCallback, never()).sendHapticFeedback(); + } - AuthenticationClient<?> client = mock(AuthenticationClient.class); - when(client.isKeyguard()).thenReturn(true); + private void initFaceAndFingerprintForBiometricPrompt() { + when(mFaceClient.isKeyguard()).thenReturn(false); + when(mFaceClient.isBiometricPrompt()).thenReturn(true); + when(mFaceClient.wasAuthAttempted()).thenReturn(true); + when(mUdfpsClient.isKeyguard()).thenReturn(false); + when(mUdfpsClient.isBiometricPrompt()).thenReturn(true); + when(mUdfpsClient.wasAuthAttempted()).thenReturn(true); + } + + @Test + public void testKeyguard_faceAuthOnly_success() { + when(mFaceClient.isKeyguard()).thenReturn(true); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); - mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, client, mCallback); + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, + mFaceClient, mCallback); verify(mCallback).sendHapticFeedback(); verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */); verify(mCallback).handleLifecycleAfterAuth(); @@ -140,21 +201,16 @@ public class CoexCoordinatorTest { @Test public void testKeyguard_faceAuth_udfpsNotTouching_faceSuccess() { - mCoexCoordinator.reset(); - - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(true); + when(mFaceClient.isKeyguard()).thenReturn(true); - AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, - withSettings().extraInterfaces(Udfps.class)); - when(udfpsClient.isKeyguard()).thenReturn(true); - when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false); + when(mUdfpsClient.isKeyguard()).thenReturn(true); + when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient); - mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient, - mCallback); + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, + mFaceClient, mCallback); // Haptics tested in #testKeyguard_bypass_haptics. Let's leave this commented out (instead // of removed) to keep this context. // verify(mCallback).sendHapticFeedback(); @@ -192,25 +248,19 @@ public class CoexCoordinatorTest { private void testKeyguard_bypass_haptics(boolean bypassEnabled, boolean faceAccepted, boolean shouldReceiveHaptics) { - mCoexCoordinator.reset(); - - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(true); - when(faceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled); + when(mFaceClient.isKeyguard()).thenReturn(true); + when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled); + when(mUdfpsClient.isKeyguard()).thenReturn(true); + when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false); - AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, - withSettings().extraInterfaces(Udfps.class)); - when(udfpsClient.isKeyguard()).thenReturn(true); - when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false); - - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient); if (faceAccepted) { - mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient, + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient, mCallback); } else { - mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient, + mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback); } @@ -244,24 +294,18 @@ public class CoexCoordinatorTest { private void testKeyguard_faceAuth_udfpsTouching_faceSuccess(boolean thenUdfpsAccepted, long udfpsRejectedAfterMs) { - mCoexCoordinator.reset(); - - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(true); + when(mFaceClient.isKeyguard()).thenReturn(true); + when(mUdfpsClient.isKeyguard()).thenReturn(true); + when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true); + when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); - AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, - withSettings().extraInterfaces(Udfps.class)); - when(udfpsClient.isKeyguard()).thenReturn(true); - when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true); - when (udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); - - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient); // For easier reading final CoexCoordinator.Callback faceCallback = mCallback; - mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, faceClient, + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mFaceClient, faceCallback); verify(faceCallback, never()).sendHapticFeedback(); verify(faceCallback, never()).sendAuthenticationResult(anyBoolean()); @@ -272,9 +316,9 @@ public class CoexCoordinatorTest { // Reset the mock CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class); assertEquals(1, mCoexCoordinator.mSuccessfulAuths.size()); - assertEquals(faceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient); + assertEquals(mFaceClient, mCoexCoordinator.mSuccessfulAuths.get(0).mAuthenticationClient); if (thenUdfpsAccepted) { - mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient, + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient, udfpsCallback); verify(udfpsCallback).sendHapticFeedback(); verify(udfpsCallback).sendAuthenticationResult(true /* addAuthTokenIfStrong */); @@ -284,7 +328,7 @@ public class CoexCoordinatorTest { assertTrue(mCoexCoordinator.mSuccessfulAuths.isEmpty()); } else { - mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, udfpsClient, + mCoexCoordinator.onAuthenticationRejected(udfpsRejectedAfterMs, mUdfpsClient, LockoutTracker.LOCKOUT_NONE, udfpsCallback); if (udfpsRejectedAfterMs <= CoexCoordinator.SUCCESSFUL_AUTH_VALID_DURATION_MS) { verify(udfpsCallback, never()).sendHapticFeedback(); @@ -310,56 +354,44 @@ public class CoexCoordinatorTest { @Test public void testKeyguard_udfpsAuthSuccess_whileFaceScanning() { - mCoexCoordinator.reset(); - - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(true); - when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mFaceClient.isKeyguard()).thenReturn(true); + when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mUdfpsClient.isKeyguard()).thenReturn(true); + when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true); - AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, - withSettings().extraInterfaces(Udfps.class)); - when(udfpsClient.isKeyguard()).thenReturn(true); - when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); - - mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, udfpsClient, + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, mUdfpsClient, mCallback); verify(mCallback).sendHapticFeedback(); verify(mCallback).sendAuthenticationResult(eq(true)); - verify(faceClient).cancel(); + verify(mFaceClient).cancel(); verify(mCallback).handleLifecycleAfterAuth(); } @Test public void testKeyguard_faceRejectedWhenUdfpsTouching_thenUdfpsRejected() { - mCoexCoordinator.reset(); - - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(true); - when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mFaceClient.isKeyguard()).thenReturn(true); + when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mUdfpsClient.isKeyguard()).thenReturn(true); + when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true); - AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, - withSettings().extraInterfaces(Udfps.class)); - when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); - when(udfpsClient.isKeyguard()).thenReturn(true); - when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); - - mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient, + mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback); verify(mCallback, never()).sendHapticFeedback(); verify(mCallback).handleLifecycleAfterAuth(); // BiometricScheduler removes the face authentication client after rejection - mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, faceClient); + mCoexCoordinator.removeAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); // Then UDFPS rejected CoexCoordinator.Callback udfpsCallback = mock(CoexCoordinator.Callback.class); - mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, udfpsClient, + mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mUdfpsClient, LockoutTracker.LOCKOUT_NONE, udfpsCallback); verify(udfpsCallback).sendHapticFeedback(); verify(udfpsCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); @@ -368,26 +400,20 @@ public class CoexCoordinatorTest { @Test public void testKeyguard_udfpsRejected_thenFaceRejected_noKeyguardBypass() { - mCoexCoordinator.reset(); - - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(true); - when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); - when(faceClient.isKeyguardBypassEnabled()).thenReturn(false); // TODO: also test "true" case + when(mFaceClient.isKeyguard()).thenReturn(true); + when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(false); // TODO: also test "true" case + when(mUdfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mUdfpsClient.isKeyguard()).thenReturn(true); + when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(true); - AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, - withSettings().extraInterfaces(Udfps.class)); - when(udfpsClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); - when(udfpsClient.isKeyguard()).thenReturn(true); - when(((Udfps) udfpsClient).isPointerDown()).thenReturn(true); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); - - mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, udfpsClient, - LockoutTracker.LOCKOUT_NONE, mCallback); + mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, + mUdfpsClient, LockoutTracker.LOCKOUT_NONE, mCallback); // Auth was attempted - when(udfpsClient.getState()) + when(mUdfpsClient.getState()) .thenReturn(AuthenticationClient.STATE_STARTED_PAUSED_ATTEMPTED); verify(mCallback, never()).sendHapticFeedback(); verify(mCallback).handleLifecycleAfterAuth(); @@ -395,7 +421,7 @@ public class CoexCoordinatorTest { // Then face rejected. Note that scheduler leaves UDFPS in the CoexCoordinator since // unlike face, its lifecycle becomes "paused" instead of "finished". CoexCoordinator.Callback faceCallback = mock(CoexCoordinator.Callback.class); - mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, faceClient, + mCoexCoordinator.onAuthenticationRejected(1 /* currentTimeMillis */, mFaceClient, LockoutTracker.LOCKOUT_NONE, faceCallback); verify(faceCallback).sendHapticFeedback(); verify(faceCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); @@ -404,20 +430,16 @@ public class CoexCoordinatorTest { @Test public void testKeyguard_capacitiveAccepted_whenFaceScanning() { - mCoexCoordinator.reset(); - - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(true); - when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); - - AuthenticationClient<?> fpClient = mock(AuthenticationClient.class); - when(fpClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); - when(fpClient.isKeyguard()).thenReturn(true); + when(mFaceClient.isKeyguard()).thenReturn(true); + when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mFingerprintClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mFingerprintClient.isKeyguard()).thenReturn(true); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, fpClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient); - mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, fpClient, mCallback); + mCoexCoordinator.onAuthenticationSucceeded(0 /* currentTimeMillis */, + mFingerprintClient, mCallback); verify(mCallback).sendHapticFeedback(); verify(mCallback).sendAuthenticationResult(eq(true) /* addAuthTokenIfStrong */); verify(mCallback).handleLifecycleAfterAuth(); @@ -425,21 +447,16 @@ public class CoexCoordinatorTest { @Test public void testKeyguard_capacitiveRejected_whenFaceScanning() { - mCoexCoordinator.reset(); - - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(true); - when(faceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); - - AuthenticationClient<?> fpClient = mock(AuthenticationClient.class); - when(fpClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); - when(fpClient.isKeyguard()).thenReturn(true); + when(mFaceClient.isKeyguard()).thenReturn(true); + when(mFaceClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mFingerprintClient.getState()).thenReturn(AuthenticationClient.STATE_STARTED); + when(mFingerprintClient.isKeyguard()).thenReturn(true); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, fpClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FP_OTHER, mFingerprintClient); - mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, fpClient, - LockoutTracker.LOCKOUT_NONE, mCallback); + mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, + mFingerprintClient, LockoutTracker.LOCKOUT_NONE, mCallback); verify(mCallback).sendHapticFeedback(); verify(mCallback).sendAuthenticationResult(eq(false) /* addAuthTokenIfStrong */); verify(mCallback).handleLifecycleAfterAuth(); @@ -447,14 +464,11 @@ public class CoexCoordinatorTest { @Test public void testNonKeyguard_rejectAndNotLockedOut() { - mCoexCoordinator.reset(); + when(mFaceClient.isKeyguard()).thenReturn(false); + when(mFaceClient.isBiometricPrompt()).thenReturn(true); - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(false); - when(faceClient.isBiometricPrompt()).thenReturn(true); - - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient, + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient, LockoutTracker.LOCKOUT_NONE, mCallback); verify(mCallback).sendHapticFeedback(); @@ -464,14 +478,11 @@ public class CoexCoordinatorTest { @Test public void testNonKeyguard_rejectLockedOut() { - mCoexCoordinator.reset(); - - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(false); - when(faceClient.isBiometricPrompt()).thenReturn(true); + when(mFaceClient.isKeyguard()).thenReturn(false); + when(mFaceClient.isBiometricPrompt()).thenReturn(true); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, faceClient, + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.onAuthenticationRejected(0 /* currentTimeMillis */, mFaceClient, LockoutTracker.LOCKOUT_TIMED, mCallback); verify(mCallback).sendHapticFeedback(); @@ -496,16 +507,13 @@ public class CoexCoordinatorTest { @Test public void testBiometricPrompt_FaceError() { - mCoexCoordinator.reset(); - - AuthenticationClient<?> client = mock(AuthenticationClient.class); - when(client.isBiometricPrompt()).thenReturn(true); - when(client.wasAuthAttempted()).thenReturn(true); + when(mFaceClient.isBiometricPrompt()).thenReturn(true); + when(mFaceClient.wasAuthAttempted()).thenReturn(true); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); - mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, - mErrorCallback); + mCoexCoordinator.onAuthenticationError(mFaceClient, + BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback); verify(mErrorCallback).sendHapticFeedback(); } @@ -520,18 +528,15 @@ public class CoexCoordinatorTest { } private void testKeyguard_faceAuthOnly(boolean bypassEnabled) { - mCoexCoordinator.reset(); - - AuthenticationClient<?> client = mock(AuthenticationClient.class); - when(client.isKeyguard()).thenReturn(true); - when(client.isKeyguardBypassEnabled()).thenReturn(bypassEnabled); - when(client.wasAuthAttempted()).thenReturn(true); - when(client.wasUserDetected()).thenReturn(true); + when(mFaceClient.isKeyguard()).thenReturn(true); + when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled); + when(mFaceClient.wasAuthAttempted()).thenReturn(true); + when(mFaceClient.wasUserDetected()).thenReturn(true); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, client); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); - mCoexCoordinator.onAuthenticationError(client, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, - mErrorCallback); + mCoexCoordinator.onAuthenticationError(mFaceClient, + BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback); verify(mErrorCallback).sendHapticFeedback(); } @@ -546,23 +551,17 @@ public class CoexCoordinatorTest { } private void testKeyguard_coex_faceError(boolean bypassEnabled) { - mCoexCoordinator.reset(); - - AuthenticationClient<?> faceClient = mock(AuthenticationClient.class); - when(faceClient.isKeyguard()).thenReturn(true); - when(faceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled); - when(faceClient.wasAuthAttempted()).thenReturn(true); - when(faceClient.wasUserDetected()).thenReturn(true); - - AuthenticationClient<?> udfpsClient = mock(AuthenticationClient.class, - withSettings().extraInterfaces(Udfps.class)); - when(udfpsClient.isKeyguard()).thenReturn(true); - when(((Udfps) udfpsClient).isPointerDown()).thenReturn(false); + when(mFaceClient.isKeyguard()).thenReturn(true); + when(mFaceClient.isKeyguardBypassEnabled()).thenReturn(bypassEnabled); + when(mFaceClient.wasAuthAttempted()).thenReturn(true); + when(mFaceClient.wasUserDetected()).thenReturn(true); + when(mUdfpsClient.isKeyguard()).thenReturn(true); + when(((Udfps) mUdfpsClient).isPointerDown()).thenReturn(false); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, faceClient); - mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, udfpsClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_FACE, mFaceClient); + mCoexCoordinator.addAuthenticationClient(SENSOR_TYPE_UDFPS, mUdfpsClient); - mCoexCoordinator.onAuthenticationError(faceClient, + mCoexCoordinator.onAuthenticationError(mFaceClient, BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, mErrorCallback); if (bypassEnabled) { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClientTest.java new file mode 100644 index 000000000000..76a5accc5fe9 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceRemovalClientTest.java @@ -0,0 +1,124 @@ +/* + * 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.server.biometrics.sensors.face.aidl; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.face.ISession; +import android.hardware.face.Face; +import android.os.IBinder; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; +import android.testing.TestableContext; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.log.BiometricLogger; +import com.android.server.biometrics.sensors.BiometricUtils; +import com.android.server.biometrics.sensors.ClientMonitorCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.HashMap; +import java.util.Map; + +@Presubmit +@SmallTest +public class FaceRemovalClientTest { + + private static final int USER_ID = 12; + + @Rule + public final TestableContext mContext = new TestableContext( + InstrumentationRegistry.getInstrumentation().getTargetContext(), null); + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private ISession mHal; + @Mock + private IBinder mToken; + @Mock + private ClientMonitorCallbackConverter mClientMonitorCallbackConverter; + @Mock + private BiometricLogger mBiometricLogger; + @Mock + private BiometricContext mBiometricContext; + @Mock + private ClientMonitorCallback mCallback; + @Mock + private Sensor.HalSessionCallback mHalSessionCallback; + @Mock + private BiometricUtils<Face> mUtils; + @Mock + private BiometricAuthenticator.Identifier mIdentifier; + private Map<Integer, Long> mAuthenticatorIds = new HashMap<Integer, Long>(); + + @Before + public void setup() { + when(mBiometricContext.updateContext(any(), anyBoolean())).thenAnswer( + i -> i.getArgument(0)); + } + + @Test + public void testFaceRemovalClient() throws RemoteException { + final int authenticatorId = 1; + int[] authenticatorIds = new int[]{authenticatorId}; + final FaceRemovalClient client = createClient(1, authenticatorIds); + when(mIdentifier.getBiometricId()).thenReturn(authenticatorId); + client.start(mCallback); + verify(mHal).removeEnrollments(authenticatorIds); + client.onRemoved(mIdentifier, 0 /* remaining */); + verify(mClientMonitorCallbackConverter).onRemoved( + eq(mIdentifier) /* identifier */, eq(0) /* remaining */); + verify(mCallback).onClientFinished(client, true); + } + + @Test + public void clientSendsErrorWhenHALFailsToRemoveEnrollment() throws RemoteException { + final FaceRemovalClient client = createClient(1, new int[0]); + client.start(mCallback); + client.onRemoved(null, 0 /* remaining */); + verify(mClientMonitorCallbackConverter).onError(eq(5) /* sensorId */, anyInt(), + eq(BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_REMOVE), eq(0) /* vendorCode*/); + verify(mCallback).onClientFinished(client, false); + } + + private FaceRemovalClient createClient(int version, int[] biometricIds) throws RemoteException { + when(mHal.getInterfaceVersion()).thenReturn(version); + final AidlSession aidl = new AidlSession(version, mHal, USER_ID, mHalSessionCallback); + return new FaceRemovalClient(mContext, () -> aidl, mToken, + mClientMonitorCallbackConverter, biometricIds, USER_ID, + "own-it", mUtils /* utils */, 5 /* sensorId */, mBiometricLogger, mBiometricContext, + mAuthenticatorIds); + } +} diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java index acb20edfe8d8..6de7fddf6ccd 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java @@ -229,8 +229,8 @@ public class LockSettingsStrongAuthTest { } private void verifyAlarm(long when, String tag, AlarmManager.OnAlarmListener alarm) { - verify(mAlarmManager).set( - eq(AlarmManager.ELAPSED_REALTIME), + verify(mAlarmManager).setExact( + eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), eq(when), eq(tag), eq(alarm), 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 16c5bfec76ae..6fe2d337e4a0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -1085,7 +1085,7 @@ public class ActivityStarterTests extends WindowTestsBase { starter.setActivityOptions(options.toBundle()) .setReason("testWindowingModeOptionsLaunchAdjacent") .setOutActivity(outActivity).execute(); - assertThat(outActivity[0].inSplitScreenWindowingMode()).isFalse(); + assertThat(outActivity[0].inMultiWindowMode()).isFalse(); // Move activity to split-screen-primary stack and make sure it has the focus. TestSplitOrganizer splitOrg = new TestSplitOrganizer(mAtm, top.getDisplayContent()); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index c08387024818..925f4f5a4130 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1914,7 +1914,7 @@ public class DisplayContentTests extends WindowTestsBase { final WindowState nextImeTargetApp = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "nextImeTargetApp"); spyOn(child1); - doReturn(true).when(child1).inSplitScreenWindowingMode(); + doReturn(false).when(mDisplayContent).shouldImeAttachedToApp(); mDisplayContent.setImeLayeringTarget(child1); spyOn(nextImeTargetApp); @@ -2418,10 +2418,10 @@ public class DisplayContentTests extends WindowTestsBase { public void testKeepClearAreasMultipleWindows() { final WindowState w1 = createWindow(null, TYPE_NAVIGATION_BAR, mDisplayContent, "w1"); final Rect rect1 = new Rect(0, 0, 10, 10); - w1.setKeepClearAreas(Arrays.asList(rect1)); + w1.setKeepClearAreas(Arrays.asList(rect1), Collections.emptyList()); final WindowState w2 = createWindow(null, TYPE_NOTIFICATION_SHADE, mDisplayContent, "w2"); final Rect rect2 = new Rect(10, 10, 20, 20); - w2.setKeepClearAreas(Arrays.asList(rect2)); + w2.setKeepClearAreas(Arrays.asList(rect2), Collections.emptyList()); // No keep clear areas on display, because the windows are not visible assertEquals(Arrays.asList(), mDisplayContent.getKeepClearAreas()); diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java index 3c3351c0b207..021568dd97b4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java @@ -451,7 +451,7 @@ public class RecentsAnimationControllerTest extends WindowTestsBase { final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation(); final int topOrientation = DisplayContentTests.getRotatedOrientation(mDefaultDisplay); assertFalse(displayRotation.updateOrientation(topOrientation, false /* forceUpdate */)); - assertEquals(recents.mOrientation, displayRotation.getLastOrientation()); + assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSET, displayRotation.getLastOrientation()); final int prevRotation = mDisplayContent.getRotation(); mWm.cleanupRecentsAnimation(REORDER_MOVE_TO_ORIGINAL_POSITION); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 501f0c4ac1b3..8474c3829827 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; @@ -691,7 +692,7 @@ public class TransitionTests extends WindowTestsBase { anyInt() /* orientation */, anyInt() /* lastRotation */); // Rotation update is skipped while the recents animation is running. assertFalse(mDisplayContent.updateRotationUnchecked()); - assertEquals(SCREEN_ORIENTATION_NOSENSOR, displayRotation.getLastOrientation()); + assertEquals(SCREEN_ORIENTATION_UNSET, displayRotation.getLastOrientation()); // Return to the app without fixed orientation from recents. app.moveFocusableActivityToTop("test"); player.finish(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index ef600f085e50..a554fab76c2b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -97,7 +97,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -992,14 +994,18 @@ public class WindowStateTests extends WindowTestsBase { final Rect keepClearArea1 = new Rect(0, 0, 10, 10); final Rect keepClearArea2 = new Rect(5, 10, 15, 20); final List<Rect> keepClearAreas = Arrays.asList(keepClearArea1, keepClearArea2); - window.setKeepClearAreas(keepClearAreas); + window.setKeepClearAreas(keepClearAreas, Collections.emptyList()); // Test that the keep-clear rects are stored and returned - assertEquals(new ArraySet(keepClearAreas), new ArraySet(window.getKeepClearAreas())); + final List<Rect> windowKeepClearAreas = new ArrayList(); + window.getKeepClearAreas(windowKeepClearAreas, new ArrayList()); + assertEquals(new ArraySet(keepClearAreas), new ArraySet(windowKeepClearAreas)); // Test that keep-clear rects are overwritten - window.setKeepClearAreas(Arrays.asList()); - assertEquals(0, window.getKeepClearAreas().size()); + window.setKeepClearAreas(Collections.emptyList(), Collections.emptyList()); + windowKeepClearAreas.clear(); + window.getKeepClearAreas(windowKeepClearAreas, new ArrayList()); + assertEquals(0, windowKeepClearAreas.size()); // Move the window position final SurfaceControl.Transaction t = spy(StubTransaction.class); @@ -1010,13 +1016,60 @@ public class WindowStateTests extends WindowTestsBase { assertEquals(new Point(frame.left, frame.top), window.mLastSurfacePosition); // Test that the returned keep-clear rects are translated to display space - window.setKeepClearAreas(keepClearAreas); + window.setKeepClearAreas(keepClearAreas, Collections.emptyList()); Rect expectedArea1 = new Rect(keepClearArea1); expectedArea1.offset(frame.left, frame.top); Rect expectedArea2 = new Rect(keepClearArea2); expectedArea2.offset(frame.left, frame.top); + windowKeepClearAreas.clear(); + window.getKeepClearAreas(windowKeepClearAreas, new ArrayList()); assertEquals(new ArraySet(Arrays.asList(expectedArea1, expectedArea2)), - new ArraySet(window.getKeepClearAreas())); + new ArraySet(windowKeepClearAreas)); + } + + @Test + public void testUnrestrictedKeepClearAreas() { + final WindowState window = createWindow(null, TYPE_APPLICATION, "window"); + makeWindowVisible(window); + + final Rect keepClearArea1 = new Rect(0, 0, 10, 10); + final Rect keepClearArea2 = new Rect(5, 10, 15, 20); + final List<Rect> keepClearAreas = Arrays.asList(keepClearArea1, keepClearArea2); + window.setKeepClearAreas(Collections.emptyList(), keepClearAreas); + + // Test that the keep-clear rects are stored and returned + final List<Rect> restrictedKeepClearAreas = new ArrayList(); + final List<Rect> unrestrictedKeepClearAreas = new ArrayList(); + window.getKeepClearAreas(restrictedKeepClearAreas, unrestrictedKeepClearAreas); + assertEquals(Collections.emptySet(), new ArraySet(restrictedKeepClearAreas)); + assertEquals(new ArraySet(keepClearAreas), new ArraySet(unrestrictedKeepClearAreas)); + + // Test that keep-clear rects are overwritten + window.setKeepClearAreas(Collections.emptyList(), Collections.emptyList()); + unrestrictedKeepClearAreas.clear(); + window.getKeepClearAreas(unrestrictedKeepClearAreas, new ArrayList()); + assertEquals(0, unrestrictedKeepClearAreas.size()); + + // Move the window position + final SurfaceControl.Transaction t = spy(StubTransaction.class); + window.mSurfaceControl = mock(SurfaceControl.class); + final Rect frame = window.getFrame(); + frame.set(10, 20, 60, 80); + window.updateSurfacePosition(t); + assertEquals(new Point(frame.left, frame.top), window.mLastSurfacePosition); + + // Test that the returned keep-clear rects are translated to display space + window.setKeepClearAreas(Collections.emptyList(), keepClearAreas); + Rect expectedArea1 = new Rect(keepClearArea1); + expectedArea1.offset(frame.left, frame.top); + Rect expectedArea2 = new Rect(keepClearArea2); + expectedArea2.offset(frame.left, frame.top); + + unrestrictedKeepClearAreas.clear(); + window.getKeepClearAreas(restrictedKeepClearAreas, unrestrictedKeepClearAreas); + assertEquals(Collections.emptySet(), new ArraySet(restrictedKeepClearAreas)); + assertEquals(new ArraySet(Arrays.asList(expectedArea1, expectedArea2)), + new ArraySet(unrestrictedKeepClearAreas)); } } diff --git a/test-runner/src/android/test/IsolatedContext.java b/test-runner/src/android/test/IsolatedContext.java index dd4a9a3a4d69..d5f92a3181b9 100644 --- a/test-runner/src/android/test/IsolatedContext.java +++ b/test-runner/src/android/test/IsolatedContext.java @@ -17,6 +17,7 @@ package android.test; import android.accounts.AccountManager; +import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; @@ -26,6 +27,7 @@ import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.net.Uri; +import android.os.Process; import android.test.mock.MockAccountManager; import java.io.File; @@ -64,6 +66,15 @@ public class IsolatedContext extends ContextWrapper { } @Override + public AttributionSource getAttributionSource() { + AttributionSource attributionSource = super.getAttributionSource(); + if (attributionSource == null) { + return new AttributionSource.Builder(Process.myUid()).build(); + } + return attributionSource; + } + + @Override public ContentResolver getContentResolver() { // We need to return the real resolver so that MailEngine.makeRight can get to the // subscribed feeds provider. TODO: mock out subscribed feeds too. diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt index c89e6a44ab6c..48b8779db112 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt @@ -97,6 +97,7 @@ open class QuickSwitchBetweenTwoAppsBackTest(private val testSpec: FlickerTestPa transitions { taplInstrumentation.launchedAppState.quickSwitchToPreviousApp() wmHelper.waitForFullScreenApp(testApp1.component) + wmHelper.waitSnapshotGone() wmHelper.waitForAppTransitionIdle() wmHelper.waitForNavBarStatusBarVisible() } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt index 5d172e2c1b14..d6c8f4601437 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt @@ -51,7 +51,7 @@ import org.junit.runners.Parameterized /** * Test quick switching back to previous app from last opened app * - * To run this test: `atest FlickerTests:QuickSwitchBetweenTwoAppsBackTest` + * To run this test: `atest FlickerTests:QuickSwitchBetweenTwoAppsForwardTest` * * Actions: * Launch an app [testApp1] @@ -101,6 +101,7 @@ open class QuickSwitchBetweenTwoAppsForwardTest(private val testSpec: FlickerTes taplInstrumentation.launchedAppState.quickSwitchToPreviousAppSwipeLeft() wmHelper.waitForFullScreenApp(testApp2.component) + wmHelper.waitSnapshotGone() wmHelper.waitForAppTransitionIdle() wmHelper.waitForNavBarStatusBarVisible() } diff --git a/tests/TrustTests/Android.bp b/tests/TrustTests/Android.bp new file mode 100644 index 000000000000..c9c6c5cf193b --- /dev/null +++ b/tests/TrustTests/Android.bp @@ -0,0 +1,39 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "TrustTests", + srcs: [ + "src/**/*.kt", + ], + static_libs: [ + "androidx.test.rules", + "androidx.test.ext.junit", + "androidx.test.uiautomator", + "truth-prebuilt", + ], + libs: [ + "android.test.runner", + "android.test.base", + ], + test_suites: [ + "device-tests", + ], + platform_apis: true, + certificate: "platform", +} diff --git a/tests/TrustTests/AndroidManifest.xml b/tests/TrustTests/AndroidManifest.xml new file mode 100644 index 000000000000..c94152da2bf6 --- /dev/null +++ b/tests/TrustTests/AndroidManifest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.trust.test" + android:targetSandboxVersion="2"> + + <uses-permission android:name="android.permission.ACCESS_KEYGUARD_SECURE_STORAGE" /> + <uses-permission android:name="android.permission.BIND_DEVICE_ADMIN" /> + <uses-permission android:name="android.permission.CONTROL_KEYGUARD" /> + <uses-permission android:name="android.permission.DEVICE_POWER" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> + <uses-permission android:name="android.permission.PROVIDE_TRUST_AGENT" /> + <uses-permission android:name="android.permission.TRUST_LISTENER" /> + + <application> + <uses-library android:name="android.test.runner"/> + <activity android:name="android.trust.TrustTestActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + + <service + android:name=".UserUnlockRequestTrustAgent" + android:exported="true" + android:label="Test Agent" + android:permission="android.permission.BIND_TRUST_AGENT"> + <intent-filter> + <action android:name="android.service.trust.TrustAgentService" /> + </intent-filter> + </service> + + <service + android:name=".LockUserTrustAgent" + android:exported="true" + android:label="Test Agent" + android:permission="android.permission.BIND_TRUST_AGENT"> + <intent-filter> + <action android:name="android.service.trust.TrustAgentService" /> + </intent-filter> + </service> + + <service + android:name=".GrantAndRevokeTrustAgent" + android:exported="true" + android:label="Test Agent" + android:permission="android.permission.BIND_TRUST_AGENT"> + <intent-filter> + <action android:name="android.service.trust.TrustAgentService" /> + </intent-filter> + </service> + </application> + + <!-- self-instrumenting test package. --> + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="android.trust.test"> + </instrumentation> +</manifest> diff --git a/tests/TrustTests/AndroidTest.xml b/tests/TrustTests/AndroidTest.xml new file mode 100644 index 000000000000..61b711eb7273 --- /dev/null +++ b/tests/TrustTests/AndroidTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<configuration description="TrustTests configuration"> + <option name="test-tag" value="TrustTests" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="TrustTests.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="android.trust.test" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false" /> + </test> +</configuration> diff --git a/tests/TrustTests/README.md b/tests/TrustTests/README.md new file mode 100644 index 000000000000..3427e30536d9 --- /dev/null +++ b/tests/TrustTests/README.md @@ -0,0 +1,40 @@ +# TrustTests framework tests + +These tests test the "trust" part of the platform primarily implemented via TrustManagerService in +the system server and TrustAgentService in system apps. + +Tests are separated into separate files based on major groupings. When creating new tests, find a +_closely_ matching existing test file or create a new test file. Prefer many test files over large +test files. + +Each test file has its own trust agent. To create a new trust agent: + +1. Create a new class extending from `BaseTrustAgentService` class in your test file +2. Add a new `<service>` stanza to `AndroidManifest.xml` in this directory for the new agent + following the pattern fo the existing agents. + +To run: + +```atest TrustTests``` + +## Testing approach: + +1. Test the agent service as a black box; avoid inspecting internal state of the service or + modifying the system code outside of this directory. +2. The primary interface to the system is through these three points: + 1. `TrustAgentService`, your agent created by the `TrustAgentRule` and accessible via + the `agent` property of the rule. + 1. Call command methods (e.g. `grantTrust`) directly on the agent + 2. Listen to events (e.g. `onUserRequestedUnlock`) by implementing the method in + your test's agent class and tracking invocations. See `UserUnlockRequestTest` for an + example. + 2. `TrustManager` which is the interface the rest of the system (e.g. SystemUI) has to the + service. + 1. Through this API, simulate system events that the service cares about + (e.g. `reportUnlockAttempt`). + 3. `TrustListener` which is the interface the rest of the system (e.g. SystemUI) uses to receive + events from the service. + 1. Through this, verify behavior that affects the rest of the system. For example, + see `LockStateTrackingRule`. +3. To re-use code between tests, prefer creating new rules alongside the existing rules or adding + functionality to a _closely_ matching existing rule. diff --git a/tests/TrustTests/src/android/trust/BaseTrustAgentService.kt b/tests/TrustTests/src/android/trust/BaseTrustAgentService.kt new file mode 100644 index 000000000000..493f3bd22d2b --- /dev/null +++ b/tests/TrustTests/src/android/trust/BaseTrustAgentService.kt @@ -0,0 +1,47 @@ +/* + * 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 android.trust + +import android.service.trust.TrustAgentService +import android.util.Log +import kotlin.reflect.KClass + +/** + * Base class for test trust agents. + */ +abstract class BaseTrustAgentService : TrustAgentService() { + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "${this::class.simpleName} created") + instances[this::class] = this + } + + override fun onDestroy() { + super.onDestroy() + instances.remove(this::class) + } + + companion object { + private val instances = + mutableMapOf<KClass<out BaseTrustAgentService>, BaseTrustAgentService>() + private const val TAG = "BaseTrustAgentService" + + fun instance(serviceClass: KClass<out BaseTrustAgentService>): BaseTrustAgentService? { + return instances[serviceClass]!! + } + } +} diff --git a/tests/TrustTests/src/android/trust/TrustTestActivity.kt b/tests/TrustTests/src/android/trust/TrustTestActivity.kt new file mode 100644 index 000000000000..6c56feace3d7 --- /dev/null +++ b/tests/TrustTests/src/android/trust/TrustTestActivity.kt @@ -0,0 +1,30 @@ +/* + * 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 android.trust + +import android.app.Activity +import android.os.Bundle + +/** + * Activity for testing Trust. + */ +class TrustTestActivity : Activity() { + + public override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + setTurnScreenOn(true) + } +} diff --git a/tests/TrustTests/src/android/trust/test/GrantAndRevokeTrustTest.kt b/tests/TrustTests/src/android/trust/test/GrantAndRevokeTrustTest.kt new file mode 100644 index 000000000000..790afd389152 --- /dev/null +++ b/tests/TrustTests/src/android/trust/test/GrantAndRevokeTrustTest.kt @@ -0,0 +1,95 @@ +/* + * 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 android.trust.test + +import android.trust.BaseTrustAgentService +import android.trust.TrustTestActivity +import android.trust.test.lib.LockStateTrackingRule +import android.trust.test.lib.ScreenLockRule +import android.trust.test.lib.TrustAgentRule +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.uiautomator.UiDevice +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +/** + * Test for testing revokeTrust & grantTrust for non-renewable trust. + * + * atest TrustTests:GrantAndRevokeTrustTest + */ +@RunWith(AndroidJUnit4::class) +class GrantAndRevokeTrustTest { + private val uiDevice = UiDevice.getInstance(getInstrumentation()) + private val activityScenarioRule = ActivityScenarioRule(TrustTestActivity::class.java) + private val lockStateTrackingRule = LockStateTrackingRule() + private val trustAgentRule = TrustAgentRule<GrantAndRevokeTrustAgent>() + + @get:Rule + val rule: RuleChain = RuleChain + .outerRule(activityScenarioRule) + .around(ScreenLockRule()) + .around(lockStateTrackingRule) + .around(trustAgentRule) + + @Before + fun manageTrust() { + trustAgentRule.agent.setManagingTrust(true) + } + + // This test serves a baseline for Grant tests, verifying that the default behavior of the + // device is to lock when put to sleep + @Test + fun sleepingDeviceWithoutGrantLocksDevice() { + uiDevice.sleep() + await() + + lockStateTrackingRule.assertLocked() + } + + @Test + fun grantKeepsDeviceUnlocked() { + trustAgentRule.agent.grantTrust(GRANT_MESSAGE, 10000, 0) + uiDevice.sleep() + await() + + lockStateTrackingRule.assertUnlocked() + } + + @Test + fun grantKeepsDeviceUnlocked_untilRevoked() { + trustAgentRule.agent.grantTrust(GRANT_MESSAGE, 0, 0) + await() + uiDevice.sleep() + trustAgentRule.agent.revokeTrust() + await() + + lockStateTrackingRule.assertLocked() + } + + companion object { + private const val TAG = "GrantAndRevokeTrustTest" + private const val GRANT_MESSAGE = "granted by test" + private fun await() = Thread.sleep(250) + } +} + +class GrantAndRevokeTrustAgent : BaseTrustAgentService() diff --git a/tests/TrustTests/src/android/trust/test/LockUserTest.kt b/tests/TrustTests/src/android/trust/test/LockUserTest.kt new file mode 100644 index 000000000000..83fc28fee818 --- /dev/null +++ b/tests/TrustTests/src/android/trust/test/LockUserTest.kt @@ -0,0 +1,68 @@ +/* + * 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 android.trust.test + +import android.trust.BaseTrustAgentService +import android.trust.TrustTestActivity +import android.trust.test.lib.LockStateTrackingRule +import android.trust.test.lib.ScreenLockRule +import android.trust.test.lib.TrustAgentRule +import android.util.Log +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +/** + * Test for testing lockUser. + * + * atest TrustTests:LockUserTest + */ +@RunWith(AndroidJUnit4::class) +class LockUserTest { + private val activityScenarioRule = ActivityScenarioRule(TrustTestActivity::class.java) + private val lockStateTrackingRule = LockStateTrackingRule() + private val trustAgentRule = TrustAgentRule<LockUserTrustAgent>() + + @get:Rule + val rule: RuleChain = RuleChain + .outerRule(activityScenarioRule) + .around(ScreenLockRule()) + .around(lockStateTrackingRule) + .around(trustAgentRule) + + @Ignore("Causes issues with subsequent tests") // TODO: Enable test + @Test + fun lockUser_locksTheDevice() { + Log.i(TAG, "Locking user") + trustAgentRule.agent.lockUser() + await() + + assertThat(lockStateTrackingRule.lockState.locked).isTrue() + } + + companion object { + private const val TAG = "LockUserTest" + private fun await() = Thread.sleep(250) + } +} + +class LockUserTrustAgent : BaseTrustAgentService() diff --git a/tests/TrustTests/src/android/trust/test/UserUnlockRequestTest.kt b/tests/TrustTests/src/android/trust/test/UserUnlockRequestTest.kt new file mode 100644 index 000000000000..f8783fbaf121 --- /dev/null +++ b/tests/TrustTests/src/android/trust/test/UserUnlockRequestTest.kt @@ -0,0 +1,81 @@ +/* + * 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 android.trust.test + +import android.app.trust.TrustManager +import android.content.Context +import android.trust.BaseTrustAgentService +import android.trust.TrustTestActivity +import android.trust.test.lib.ScreenLockRule +import android.trust.test.lib.TrustAgentRule +import android.util.Log +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +/** + * Test for testing the user unlock trigger. + * + * atest TrustTests:UserUnlockRequestTest + */ +@RunWith(AndroidJUnit4::class) +class UserUnlockRequestTest { + private val context: Context = getApplicationContext() + private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager + private val userId = context.userId + private val activityScenarioRule = ActivityScenarioRule(TrustTestActivity::class.java) + private val trustAgentRule = TrustAgentRule<UserUnlockRequestTrustAgent>() + + @get:Rule + val rule: RuleChain = RuleChain + .outerRule(activityScenarioRule) + .around(ScreenLockRule()) + .around(trustAgentRule) + + @Test + fun reportUserRequestedUnlock_propagatesToAgent() { + val oldCount = trustAgentRule.agent.onUserRequestedUnlockCallCount + trustManager.reportUserRequestedUnlock(userId) + await() + + assertThat(trustAgentRule.agent.onUserRequestedUnlockCallCount) + .isEqualTo(oldCount + 1) + } + + companion object { + private const val TAG = "UserUnlockRequestTest" + private fun await() = Thread.sleep(250) + } +} + +class UserUnlockRequestTrustAgent : BaseTrustAgentService() { + var onUserRequestedUnlockCallCount: Long = 0 + private set + + override fun onUserRequestedUnlock() { + Log.i(TAG, "onUserRequestedUnlock") + onUserRequestedUnlockCallCount++ + } + + companion object { + private const val TAG = "UserUnlockRequestTrustAgent" + } +} diff --git a/tests/TrustTests/src/android/trust/test/lib/LockStateTrackingRule.kt b/tests/TrustTests/src/android/trust/test/lib/LockStateTrackingRule.kt new file mode 100644 index 000000000000..0023af8893e2 --- /dev/null +++ b/tests/TrustTests/src/android/trust/test/lib/LockStateTrackingRule.kt @@ -0,0 +1,83 @@ +/* + * 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 android.trust.test.lib + +import android.app.trust.TrustManager +import android.app.trust.TrustManager.TrustListener +import android.content.Context +import android.util.Log +import android.view.WindowManagerGlobal +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import com.google.common.truth.Truth.assertThat +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Rule for tracking the lock state of the device based on events emitted to [TrustListener]. + */ +class LockStateTrackingRule : TestRule { + private val context: Context = getApplicationContext() + private val windowManager = WindowManagerGlobal.getWindowManagerService() + + @Volatile lateinit var lockState: LockState + private set + + override fun apply(base: Statement, description: Description) = object : Statement() { + override fun evaluate() { + lockState = LockState(locked = windowManager.isKeyguardLocked) + val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager + val listener = Listener() + + trustManager.registerTrustListener(listener) + try { + base.evaluate() + } finally { + trustManager.unregisterTrustListener(listener) + } + } + } + + fun assertLocked() = assertThat(lockState.locked).isTrue() + fun assertUnlocked() = assertThat(lockState.locked).isFalse() + + inner class Listener : TrustListener { + override fun onTrustChanged( + enabled: Boolean, + userId: Int, + flags: Int, + trustGrantedMessages: MutableList<String> + ) { + Log.d(TAG, "Device became trusted=$enabled") + lockState = lockState.copy(locked = !enabled) + } + + override fun onTrustManagedChanged(enabled: Boolean, userId: Int) { + } + + override fun onTrustError(message: CharSequence) { + } + } + + data class LockState( + val locked: Boolean? = null + ) + + companion object { + private const val TAG = "LockStateTrackingRule" + } +} diff --git a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt new file mode 100644 index 000000000000..c682a00eb8b9 --- /dev/null +++ b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt @@ -0,0 +1,105 @@ +/* + * 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 android.trust.test.lib + +import android.content.Context +import android.util.Log +import android.view.WindowManagerGlobal +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockscreenCredential +import com.google.common.truth.Truth.assertWithMessage +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Sets a screen lock on the device for the duration of the test. + */ +class ScreenLockRule : TestRule { + private val context: Context = getApplicationContext() + private val windowManager = WindowManagerGlobal.getWindowManagerService() + private val lockPatternUtils = LockPatternUtils(context) + private var instantLockSavedValue = false + + override fun apply(base: Statement, description: Description) = object : Statement() { + override fun evaluate() { + verifyNoScreenLockAlreadySet() + verifyKeyguardDismissed() + setScreenLock() + setLockOnPowerButton() + + try { + base.evaluate() + } finally { + removeScreenLock() + revertLockOnPowerButton() + } + } + } + + private fun verifyNoScreenLockAlreadySet() { + assertWithMessage("Screen Lock must not already be set on device") + .that(lockPatternUtils.isSecure(context.userId)) + .isFalse() + } + + private fun verifyKeyguardDismissed() { + windowManager.dismissKeyguard(null, null) + Thread.sleep(250) + assertWithMessage("Keyguard should be unlocked") + .that(windowManager.isKeyguardLocked) + .isFalse() + } + + private fun setScreenLock() { + lockPatternUtils.setLockCredential( + LockscreenCredential.createPin(PIN), + LockscreenCredential.createNone(), + context.userId + ) + assertWithMessage("Screen Lock should now be set") + .that(lockPatternUtils.isSecure(context.userId)) + .isTrue() + Log.i(TAG, "Device PIN set to $PIN") + } + + private fun setLockOnPowerButton() { + instantLockSavedValue = lockPatternUtils.getPowerButtonInstantlyLocks(context.userId) + lockPatternUtils.setPowerButtonInstantlyLocks(true, context.userId) + } + + private fun removeScreenLock() { + lockPatternUtils.setLockCredential( + LockscreenCredential.createNone(), + LockscreenCredential.createPin(PIN), + context.userId + ) + Log.i(TAG, "Device PIN cleared; waiting 50 ms then dismissing Keyguard") + Thread.sleep(50) + windowManager.dismissKeyguard(null, null) + } + + private fun revertLockOnPowerButton() { + lockPatternUtils.setPowerButtonInstantlyLocks(instantLockSavedValue, context.userId) + } + + companion object { + private const val TAG = "ScreenLockRule" + private const val PIN = "0000" + } +} diff --git a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt new file mode 100644 index 000000000000..2a9e00276475 --- /dev/null +++ b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt @@ -0,0 +1,117 @@ +/* + * 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 android.trust.test.lib + +import android.app.trust.TrustManager +import android.content.ComponentName +import android.content.Context +import android.trust.BaseTrustAgentService +import android.util.Log +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import com.android.internal.widget.LockPatternUtils +import com.google.common.truth.Truth.assertWithMessage +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import kotlin.reflect.KClass + +/** + * Enables a trust agent and causes the system service to bind to it. + * + * The enabled agent can be accessed during the test via the [agent] property. + * + * @constructor Creates the rule. Do not use; instead, use [invoke]. + */ +class TrustAgentRule<T : BaseTrustAgentService>( + private val serviceClass: KClass<T> +) : TestRule { + private val context: Context = getApplicationContext() + private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager + private val lockPatternUtils = LockPatternUtils(context) + + val agent get() = BaseTrustAgentService.instance(serviceClass) as T + + override fun apply(base: Statement, description: Description) = object : Statement() { + override fun evaluate() { + verifyTrustServiceRunning() + unlockDeviceWithCredential() + enableTrustAgent() + waitForEnablement() + + try { + verifyAgentIsRunning() + base.evaluate() + } finally { + disableTrustAgent() + } + } + } + + private fun verifyTrustServiceRunning() { + assertWithMessage("Trust service is not running").that(trustManager).isNotNull() + } + + private fun unlockDeviceWithCredential() { + Log.d(TAG, "Unlocking device with credential") + trustManager.reportUnlockAttempt(true, context.userId) + } + + private fun enableTrustAgent() { + val componentName = ComponentName(context, serviceClass.java) + val userId = context.userId + Log.i(TAG, "Enabling trust agent ${componentName.flattenToString()} for user $userId") + val agents = mutableListOf(componentName) + .plus(lockPatternUtils.getEnabledTrustAgents(userId)) + .distinct() + lockPatternUtils.setEnabledTrustAgents(agents, userId) + } + + private fun waitForEnablement() { + Log.d(TAG, "Waiting for $WAIT_TIME ms") + Thread.sleep(WAIT_TIME) + Log.d(TAG, "Done waiting") + } + + private fun verifyAgentIsRunning() { + assertWithMessage("${serviceClass.simpleName} should be running") + .that(BaseTrustAgentService.instance(serviceClass)).isNotNull() + } + + private fun disableTrustAgent() { + val componentName = ComponentName(context, serviceClass.java) + val userId = context.userId + Log.i(TAG, "Disabling trust agent ${componentName.flattenToString()} for user $userId") + val agents = lockPatternUtils.getEnabledTrustAgents(userId).toMutableList() + .distinct() + .minus(componentName) + lockPatternUtils.setEnabledTrustAgents(agents, userId) + } + + companion object { + /** + * Creates a new rule for the specified agent class. Example usage: + * ``` + * @get:Rule val rule = TrustAgentRule<MyTestAgent>() + * ``` + */ + inline operator fun <reified T : BaseTrustAgentService> invoke() = + TrustAgentRule(T::class) + + private const val TAG = "TrustAgentRule" + private val WAIT_TIME = 1000L + } +} |