diff options
319 files changed, 8488 insertions, 3517 deletions
diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 70a23cdf106b..93c0c4d7a024 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1139,7 +1139,6 @@ package android.hardware.camera2 { public final class CameraManager { method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; - field public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; // 0xef10e60L } public abstract static class CameraManager.AvailabilityCallback { diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 4a4ba6371939..bab20615cf77 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -6787,6 +6787,8 @@ public class DevicePolicyManager { * {@link #ENCRYPTION_STATUS_UNSUPPORTED}, {@link #ENCRYPTION_STATUS_INACTIVE}, * {@link #ENCRYPTION_STATUS_ACTIVATING}, {@link #ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY}, * {@link #ENCRYPTION_STATUS_ACTIVE}, or {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}. + * + * @throws SecurityException if called on a parent instance. */ public int getStorageEncryptionStatus() { throwIfParentInstance("getStorageEncryptionStatus"); diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 2ea0d8235548..a320f1e9509c 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -11488,7 +11488,7 @@ public class Intent implements Parcelable, Cloneable { private void toUriInner(StringBuilder uri, String scheme, String defAction, String defPackage, int flags) { if (scheme != null) { - uri.append("scheme=").append(scheme).append(';'); + uri.append("scheme=").append(Uri.encode(scheme)).append(';'); } if (mAction != null && !mAction.equals(defAction)) { uri.append("action=").append(Uri.encode(mAction)).append(';'); diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 5291d2b73891..7ccf07a2a764 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -29,7 +29,6 @@ import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.ActivityThread; import android.app.AppOpsManager; -import android.app.compat.CompatChanges; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.graphics.ImageFormat; @@ -47,7 +46,6 @@ import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemProperties; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RSIllegalArgumentException; @@ -284,14 +282,6 @@ public class Camera { */ public native static int getNumberOfCameras(); - private static final boolean sLandscapeToPortrait = - SystemProperties.getBoolean(CameraManager.LANDSCAPE_TO_PORTRAIT_PROP, false); - - private static boolean shouldOverrideToPortrait() { - return CompatChanges.isChangeEnabled(CameraManager.OVERRIDE_FRONT_CAMERA_APP_COMPAT) - && sLandscapeToPortrait; - } - /** * Returns the information about a particular camera. * If {@link #getNumberOfCameras()} returns N, the valid id is 0 to N-1. @@ -301,7 +291,8 @@ public class Camera { * low-level failure). */ public static void getCameraInfo(int cameraId, CameraInfo cameraInfo) { - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait( + ActivityThread.currentApplication().getApplicationContext()); _getCameraInfo(cameraId, overrideToPortrait, cameraInfo); IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); @@ -498,7 +489,8 @@ public class Camera { mEventHandler = null; } - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait( + ActivityThread.currentApplication().getApplicationContext()); return native_setup(new WeakReference<Camera>(this), cameraId, ActivityThread.currentOpPackageName(), overrideToPortrait); } diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index be99f0fefee7..5e2b40cbaf75 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -115,8 +115,14 @@ public final class CameraManager { @ChangeId @Overridable @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) - @TestApi - public static final long OVERRIDE_FRONT_CAMERA_APP_COMPAT = 250678880L; + public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; + + /** + * Package-level opt in/out for the above. + * @hide + */ + public static final String PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT = + "android.camera.PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT"; /** * System property for allowing the above @@ -602,7 +608,7 @@ public final class CameraManager { try { Size displaySize = getDisplaySize(); - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = shouldOverrideToPortrait(mContext); CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId, mContext.getApplicationInfo().targetSdkVersion, overrideToPortrait); try { @@ -722,7 +728,7 @@ public final class CameraManager { "Camera service is currently unavailable"); } - boolean overrideToPortrait = shouldOverrideToPortrait(); + boolean overrideToPortrait = shouldOverrideToPortrait(mContext); cameraUser = cameraService.connectDevice(callbacks, cameraId, mContext.getOpPackageName(), mContext.getAttributionTag(), uid, oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion, @@ -1154,9 +1160,26 @@ public final class CameraManager { return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId); } - private static boolean shouldOverrideToPortrait() { - return CompatChanges.isChangeEnabled(OVERRIDE_FRONT_CAMERA_APP_COMPAT) - && CameraManagerGlobal.sLandscapeToPortrait; + /** + * @hide + */ + public static boolean shouldOverrideToPortrait(@Nullable Context context) { + if (!CameraManagerGlobal.sLandscapeToPortrait) { + return false; + } + + if (context != null) { + PackageManager packageManager = context.getPackageManager(); + + try { + return packageManager.getProperty(context.getOpPackageName(), + PROPERTY_COMPAT_OVERRIDE_LANDSCAPE_TO_PORTRAIT).getBoolean(); + } catch (PackageManager.NameNotFoundException e) { + // No such property + } + } + + return CompatChanges.isChangeEnabled(OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT); } /** @@ -2313,6 +2336,15 @@ public final class CameraManager { final AvailabilityCallback callback = mCallbackMap.keyAt(i); postSingleUpdate(callback, executor, id, null /*physicalId*/, status); + + // Send the NOT_PRESENT state for unavailable physical cameras + if (isAvailable(status) && mUnavailablePhysicalDevices.containsKey(id)) { + ArrayList<String> unavailableIds = mUnavailablePhysicalDevices.get(id); + for (String unavailableId : unavailableIds) { + postSingleUpdate(callback, executor, id, unavailableId, + ICameraServiceListener.STATUS_NOT_PRESENT); + } + } } } // onStatusChangedLocked diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index a6c79b3a289f..0c2468e65577 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -87,6 +87,7 @@ public class CameraDeviceImpl extends CameraDevice // TODO: guard every function with if (!mRemoteDevice) check (if it was closed) private ICameraDeviceUserWrapper mRemoteDevice; + private boolean mRemoteDeviceInit = false; // Lock to synchronize cross-thread access to device public interface final Object mInterfaceLock = new Object(); // access from this class and Session only! @@ -338,6 +339,8 @@ public class CameraDeviceImpl extends CameraDevice mDeviceExecutor.execute(mCallOnOpened); mDeviceExecutor.execute(mCallOnUnconfigured); + + mRemoteDeviceInit = true; } } @@ -1754,8 +1757,8 @@ public class CameraDeviceImpl extends CameraDevice } synchronized(mInterfaceLock) { - if (mRemoteDevice == null) { - return; // Camera already closed + if (mRemoteDevice == null && mRemoteDeviceInit) { + return; // Camera already closed, user is not interested in errors anymore. } // Redirect device callback to the offline session in case we are in the middle diff --git a/core/java/android/hardware/devicestate/DeviceStateManager.java b/core/java/android/hardware/devicestate/DeviceStateManager.java index dba1a5e8dfc6..6a667fe39974 100644 --- a/core/java/android/hardware/devicestate/DeviceStateManager.java +++ b/core/java/android/hardware/devicestate/DeviceStateManager.java @@ -251,6 +251,10 @@ public final class DeviceStateManager { @Nullable private Boolean lastResult; + public FoldStateListener(Context context) { + this(context, folded -> {}); + } + public FoldStateListener(Context context, Consumer<Boolean> listener) { mFoldedDeviceStates = context.getResources().getIntArray( com.android.internal.R.array.config_foldedDeviceStates); @@ -266,5 +270,10 @@ public final class DeviceStateManager { mDelegate.accept(folded); } } + + @Nullable + public Boolean getFolded() { + return lastResult; + } } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 94a6382227f3..b21187af1271 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9819,11 +9819,10 @@ public final class Settings { "fingerprint_side_fps_auth_downtime"; /** - * Whether or not a SFPS device is required to be interactive for auth to unlock the device. + * Whether or not a SFPS device is enabling the performant auth setting. * @hide */ - public static final String SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED = - "sfps_require_screen_on_to_auth_enabled"; + public static final String SFPS_PERFORMANT_AUTH_ENABLED = "sfps_performant_auth_enabled"; /** * Whether or not debugging is enabled. diff --git a/core/java/android/text/TextShaper.java b/core/java/android/text/TextShaper.java index a1d6cc8e283a..6da0b63dbc1f 100644 --- a/core/java/android/text/TextShaper.java +++ b/core/java/android/text/TextShaper.java @@ -173,7 +173,7 @@ public class TextShaper { private TextShaper() {} /** - * An consumer interface for accepting text shape result. + * A consumer interface for accepting text shape result. */ public interface GlyphsConsumer { /** diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index d067d4bc366b..497f0668107f 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -66,8 +66,7 @@ import java.util.concurrent.Executor; import java.util.function.Consumer; /** - * <p>The {@link ContentCaptureManager} provides additional ways for for apps to - * integrate with the content capture subsystem. + * <p>Provides additional ways for apps to integrate with the content capture subsystem. * * <p>Content capture provides real-time, continuous capture of application activity, display and * events to an intelligence service that is provided by the Android system. The intelligence diff --git a/core/java/android/webkit/WebResourceError.java b/core/java/android/webkit/WebResourceError.java index 11f1b6f17566..4c874892d576 100644 --- a/core/java/android/webkit/WebResourceError.java +++ b/core/java/android/webkit/WebResourceError.java @@ -19,7 +19,7 @@ package android.webkit; import android.annotation.SystemApi; /** - * Encapsulates information about errors occured during loading of web resources. See + * Encapsulates information about errors that occurred during loading of web resources. See * {@link WebViewClient#onReceivedError(WebView, WebResourceRequest, WebResourceError) WebViewClient.onReceivedError(WebView, WebResourceRequest, WebResourceError)} */ public abstract class WebResourceError { diff --git a/core/java/android/window/ITaskOrganizerController.aidl b/core/java/android/window/ITaskOrganizerController.aidl index e6bb1f64ad86..0032b9ce0512 100644 --- a/core/java/android/window/ITaskOrganizerController.aidl +++ b/core/java/android/window/ITaskOrganizerController.aidl @@ -40,7 +40,8 @@ interface ITaskOrganizerController { void unregisterTaskOrganizer(ITaskOrganizer organizer); /** Creates a persistent root task in WM for a particular windowing-mode. */ - void createRootTask(int displayId, int windowingMode, IBinder launchCookie); + void createRootTask(int displayId, int windowingMode, IBinder launchCookie, + boolean removeWithTaskOrganizer); /** Deletes a persistent root task in WM */ boolean deleteRootTask(in WindowContainerToken task); diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java index bffd4e437dfa..02878f8ae72b 100644 --- a/core/java/android/window/TaskOrganizer.java +++ b/core/java/android/window/TaskOrganizer.java @@ -152,17 +152,33 @@ public class TaskOrganizer extends WindowOrganizer { * @param windowingMode Windowing mode to put the root task in. * @param launchCookie Launch cookie to associate with the task so that is can be identified * when the {@link ITaskOrganizer#onTaskAppeared} callback is called. + * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed. + * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) - @Nullable - public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { try { - mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie); + mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie, + removeWithTaskOrganizer); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param launchCookie Launch cookie to associate with the task so that is can be identified + * when the {@link ITaskOrganizer#onTaskAppeared} callback is called. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) + @Nullable + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + createRootTask(displayId, windowingMode, launchCookie, false /* removeWithTaskOrganizer */); + } + /** Deletes a persistent root task in WM */ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public boolean deleteRootTask(@NonNull WindowContainerToken task) { diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 1fcfe7dd5b6f..011232fe1915 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -2953,12 +2953,24 @@ public class ChooserActivity extends ResolverActivity implements private boolean shouldShowStickyContentPreviewNoOrientationCheck() { return shouldShowTabs() - && mMultiProfilePagerAdapter.getListAdapterForUserHandle( - UserHandle.of(UserHandle.myUserId())).getCount() > 0 + && (mMultiProfilePagerAdapter.getListAdapterForUserHandle( + UserHandle.of(UserHandle.myUserId())).getCount() > 0 + || shouldShowContentPreviewWhenEmpty()) && shouldShowContentPreview(); } /** + * This method could be used to override the default behavior when we hide the preview area + * when the current tab doesn't have any items. + * + * @return true if we want to show the content preview area even if the tab for the current + * user is empty + */ + protected boolean shouldShowContentPreviewWhenEmpty() { + return false; + } + + /** * @return true if we want to show the content preview area */ protected boolean shouldShowContentPreview() { diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index f8b764be582b..19e4ba405feb 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -209,7 +209,7 @@ public class ResolverActivity extends Activity implements * <p>Can only be used if there is a work profile. * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}. */ - static final String EXTRA_SELECTED_PROFILE = + protected static final String EXTRA_SELECTED_PROFILE = "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE"; /** @@ -224,8 +224,8 @@ public class ResolverActivity extends Activity implements static final String EXTRA_CALLING_USER = "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER"; - static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL; - static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK; + protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL; + protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK; private BroadcastReceiver mWorkProfileStateReceiver; private UserHandle mHeaderCreatorUser; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 6e4871fdf922..668a7d5aa9b6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -555,9 +555,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator = mController.getSplitAttributesCalculator(); final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes(); - final boolean isDefaultMinSizeSatisfied = rule.checkParentMetrics(taskWindowMetrics); + final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics); if (calculator == null) { - if (!isDefaultMinSizeSatisfied) { + if (!areDefaultConstraintsSatisfied) { return EXPAND_CONTAINERS_ATTRIBUTES; } return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes, @@ -568,7 +568,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { taskConfiguration.windowConfiguration); final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams( taskWindowMetrics, taskConfiguration, windowLayoutInfo, defaultSplitAttributes, - isDefaultMinSizeSatisfied, rule.getTag()); + areDefaultConstraintsSatisfied, rule.getTag()); final SplitAttributes splitAttributes = calculator.apply(params); return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair); } diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differindex 367e3b9d9676..5de5365e4d03 100644 --- a/libs/WindowManager/Jetpack/window-extensions-release.aar +++ b/libs/WindowManager/Jetpack/window-extensions-release.aar diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index e58e785850fa..97a9fede22d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -256,12 +256,30 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param listener The listener to get the created task callback. + */ public void createRootTask(int displayId, int windowingMode, TaskListener listener) { - ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s", + createRootTask(displayId, windowingMode, listener, false /* removeWithTaskOrganizer */); + } + + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param listener The listener to get the created task callback. + * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed. + */ + public void createRootTask(int displayId, int windowingMode, TaskListener listener, + boolean removeWithTaskOrganizer) { + ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s" , displayId, windowingMode, listener.toString()); final IBinder cookie = new Binder(); setPendingLaunchCookieListener(cookie, listener); - super.createRootTask(displayId, windowingMode, cookie); + super.createRootTask(displayId, windowingMode, cookie, removeWithTaskOrganizer); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index dd8afff0df2c..71e15c12b9c0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -973,21 +973,59 @@ public class BubbleController implements ConfigurationChangeListener { } /** - * Adds and expands bubble for a specific intent. These bubbles are <b>not</b> backed by a n - * otification and remain until the user dismisses the bubble or bubble stack. Only one intent - * bubble is supported at a time. + * This method has different behavior depending on: + * - if an app bubble exists + * - if an app bubble is expanded + * + * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * intent must be explicit (i.e. include a package name or fully qualified component class name) + * and the activity for it should be resizable. + * + * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is + * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * this method will expand it. + * + * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses + * the bubble or bubble stack. + * + * Some notes: + * - Only one app bubble is supported at a time + * - Calling this method with a different intent than the existing app bubble will do nothing * * @param intent the intent to display in the bubble expanded view. */ - public void showAppBubble(Intent intent) { - if (intent == null || intent.getPackage() == null) return; + public void showOrHideAppBubble(Intent intent) { + if (intent == null || intent.getPackage() == null) { + Log.w(TAG, "App bubble failed to show, invalid intent: " + intent + + ((intent != null) ? " with package: " + intent.getPackage() : " ")); + return; + } PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId); if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return; - Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor); - b.setShouldAutoExpand(true); - inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE); + if (existingAppBubble != null) { + BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); + if (isStackExpanded()) { + if (selectedBubble != null && KEY_APP_BUBBLE.equals(selectedBubble.getKey())) { + // App bubble is expanded, lets collapse + collapseStack(); + } else { + // App bubble is not selected, select it + mBubbleData.setSelectedBubble(existingAppBubble); + } + } else { + // App bubble is not selected, select it & expand + mBubbleData.setSelectedBubble(existingAppBubble); + mBubbleData.setExpanded(true); + } + } else { + // App bubble does not exist, lets add and expand it + Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor); + b.setShouldAutoExpand(true); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } } /** @@ -1697,9 +1735,9 @@ public class BubbleController implements ConfigurationChangeListener { } @Override - public void showAppBubble(Intent intent) { + public void showOrHideAppBubble(Intent intent) { mMainExecutor.execute(() -> { - BubbleController.this.showAppBubble(intent); + BubbleController.this.showOrHideAppBubble(intent); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index af31391fec96..6230d22ebe12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -17,6 +17,7 @@ package com.android.wm.shell.bubbles; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_DATA; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -684,7 +685,8 @@ public class BubbleData { if (bubble.getPendingIntentCanceled() || !(reason == Bubbles.DISMISS_AGED || reason == Bubbles.DISMISS_USER_GESTURE - || reason == Bubbles.DISMISS_RELOAD_FROM_DISK)) { + || reason == Bubbles.DISMISS_RELOAD_FROM_DISK) + || KEY_APP_BUBBLE.equals(bubble.getKey())) { return; } if (DEBUG_BUBBLE_DATA) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 465d1abe0a3d..df4325763a17 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -109,13 +109,28 @@ public interface Bubbles { void expandStackAndSelectBubble(Bubble bubble); /** - * Adds and expands bubble that is not notification based, but instead based on an intent from - * the app. The intent must be explicit (i.e. include a package name or fully qualified - * component class name) and the activity for it should be resizable. + * This method has different behavior depending on: + * - if an app bubble exists + * - if an app bubble is expanded * - * @param intent the intent to populate the bubble. + * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * intent must be explicit (i.e. include a package name or fully qualified component class name) + * and the activity for it should be resizable. + * + * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is + * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * this method will expand it. + * + * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses + * the bubble or bubble stack. + * + * Some notes: + * - Only one app bubble is supported at a time + * - Calling this method with a different intent than the existing app bubble will do nothing + * + * @param intent the intent to display in the bubble expanded view. */ - void showAppBubble(Intent intent); + void showOrHideAppBubble(Intent intent); /** * @return a bubble that matches the provided shortcutId, if one exists. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index e6c7e101d078..83158ffafa7e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -662,8 +662,8 @@ public class PipTransition extends PipTransitionController { } // Please file a bug to handle the unexpected transition type. - throw new IllegalStateException("Entering PIP with unexpected transition type=" - + transitTypeToString(transitType)); + android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type=" + + transitTypeToString(transitType), new Throwable()); } return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index 281ea530e9e1..431bd7b08142 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -333,6 +333,9 @@ public class PhonePipMenuController implements PipMenuController { mTmpDestinationRectF.set(destinationBounds); mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); final SurfaceControl surfaceControl = getSurfaceControl(); + if (surfaceControl == null) { + return; + } final SurfaceControl.Transaction menuTx = mSurfaceControlTransactionFactory.getTransaction(); menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform); @@ -359,6 +362,9 @@ public class PhonePipMenuController implements PipMenuController { } final SurfaceControl surfaceControl = getSurfaceControl(); + if (surfaceControl == null) { + return; + } final SurfaceControl.Transaction menuTx = mSurfaceControlTransactionFactory.getTransaction(); menuTx.setCrop(surfaceControl, destinationBounds); 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 8ddc3c04d991..1488469759cd 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 @@ -605,9 +605,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); + if (taskId2 == INVALID_TASK_ID) { + // Launching a solo task. + ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); + activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); + options1 = activityOptions.toBundle(); + addActivityOptions(options1, null /* launchTarget */); + wct.startTask(taskId1, options1); + mSyncQueue.queue(wct); + return; + } + addActivityOptions(options1, mSideStage); wct.startTask(taskId1, options1); - startWithLegacyTransition(wct, taskId2, options2, splitPosition, splitRatio, adapter, instanceId); } @@ -632,9 +642,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, InstanceId instanceId) { final WindowContainerTransaction wct = new WindowContainerTransaction(); if (options1 == null) options1 = new Bundle(); + if (taskId == INVALID_TASK_ID) { + // Launching a solo task. + ActivityOptions activityOptions = ActivityOptions.fromBundle(options1); + activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter)); + options1 = activityOptions.toBundle(); + addActivityOptions(options1, null /* launchTarget */); + wct.sendPendingIntent(pendingIntent, fillInIntent, options1); + mSyncQueue.queue(wct); + return; + } + addActivityOptions(options1, mSideStage); wct.sendPendingIntent(pendingIntent, fillInIntent, options1); - startWithLegacyTransition(wct, taskId, options2, splitPosition, splitRatio, adapter, instanceId); } @@ -696,6 +716,34 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mShouldUpdateRecents = false; mIsSplitEntering = true; + setSideStagePosition(sidePosition, wct); + if (!mMainStage.isActive()) { + mMainStage.activate(wct, false /* reparent */); + } + + if (mainOptions == null) mainOptions = new Bundle(); + addActivityOptions(mainOptions, mMainStage); + mainOptions = wrapAsSplitRemoteAnimation(adapter, mainOptions); + + updateWindowBounds(mSplitLayout, wct); + if (mainTaskId == INVALID_TASK_ID) { + wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions); + } else { + wct.startTask(mainTaskId, mainOptions); + } + + wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); + + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + setDividerVisibility(true, t); + }); + + setEnterInstanceId(instanceId); + } + + private Bundle wrapAsSplitRemoteAnimation(RemoteAnimationAdapter adapter, Bundle options) { final WindowContainerTransaction evictWct = new WindowContainerTransaction(); if (isSplitScreenVisible()) { mMainStage.evictAllChildren(evictWct); @@ -739,37 +787,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, }; RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter( wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay()); - - if (mainOptions == null) { - mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle(); - } else { - ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions); - mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); - mainOptions = mainActivityOptions.toBundle(); - } - - setSideStagePosition(sidePosition, wct); - if (!mMainStage.isActive()) { - mMainStage.activate(wct, false /* reparent */); - } - - if (mainOptions == null) mainOptions = new Bundle(); - addActivityOptions(mainOptions, mMainStage); - updateWindowBounds(mSplitLayout, wct); - if (mainTaskId == INVALID_TASK_ID) { - wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions); - } else { - wct.startTask(mainTaskId, mainOptions); - } - wct.reorder(mRootTaskInfo.token, true); - wct.setForceTranslucent(mRootTaskInfo.token, false); - - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - setDividerVisibility(true, t); - }); - - setEnterInstanceId(instanceId); + ActivityOptions activityOptions = ActivityOptions.fromBundle(options); + activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); + return activityOptions.toBundle(); } private void setEnterInstanceId(InstanceId instanceId) { @@ -1228,8 +1248,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return SPLIT_POSITION_UNDEFINED; } - private void addActivityOptions(Bundle opts, StageTaskListener stage) { - opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); + private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) { + if (launchTarget != null) { + opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, launchTarget.mRootTaskInfo.token); + } // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split // will be canceled. opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index e6711aca19c1..8b025cd7c246 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; + import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -32,6 +34,7 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; +import android.content.Intent; import android.content.LocusId; import android.graphics.drawable.Icon; import android.os.Bundle; @@ -94,6 +97,7 @@ public class BubbleDataTest extends ShellTestCase { private Bubble mBubbleInterruptive; private Bubble mBubbleDismissed; private Bubble mBubbleLocusId; + private Bubble mAppBubble; private BubbleData mBubbleData; private TestableBubblePositioner mPositioner; @@ -178,6 +182,11 @@ public class BubbleDataTest extends ShellTestCase { mBubbleMetadataFlagListener, mPendingIntentCanceledListener, mMainExecutor); + + Intent appBubbleIntent = new Intent(mContext, BubblesTestActivity.class); + appBubbleIntent.setPackage(mContext.getPackageName()); + mAppBubble = new Bubble(appBubbleIntent, new UserHandle(1), mMainExecutor); + mPositioner = new TestableBubblePositioner(mContext, mock(WindowManager.class)); mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner, @@ -1089,6 +1098,18 @@ public class BubbleDataTest extends ShellTestCase { assertOverflowChangedTo(ImmutableList.of()); } + @Test + public void test_removeAppBubble_skipsOverflow() { + mBubbleData.notificationEntryUpdated(mAppBubble, true /* suppressFlyout*/, + false /* showInShade */); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isEqualTo(mAppBubble); + + mBubbleData.dismissBubbleWithKey(KEY_APP_BUBBLE, Bubbles.DISMISS_USER_GESTURE); + + assertThat(mBubbleData.getOverflowBubbleWithKey(KEY_APP_BUBBLE)).isNull(); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull(); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedBackupTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedBackupTaskTest.java index f6914efd6d83..23d6e34db4f1 100644 --- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedBackupTaskTest.java +++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedBackupTaskTest.java @@ -22,8 +22,8 @@ import static com.android.server.backup.encryption.protos.nano.ChunksMetadataPro import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java index 096b2da10c98..bfc5d0dca3ff 100644 --- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java +++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java @@ -18,9 +18,9 @@ package com.android.server.backup.encryption.tasks; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.any; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java index fa4fef50ac1a..222b88221ba2 100644 --- a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java +++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java @@ -19,8 +19,8 @@ package com.android.server.backup.encryption.tasks; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertFalse; @@ -41,13 +41,6 @@ import com.android.server.backup.encryption.protos.nano.KeyValueListingProto; import com.android.server.backup.encryption.protos.nano.WrappedKeyProto; import com.android.server.backup.testing.CryptoTestUtils; -import java.io.IOException; -import java.util.Arrays; -import java.util.Map; -import java.util.Map.Entry; - -import javax.crypto.SecretKey; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -59,6 +52,14 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.Map.Entry; + +import javax.crypto.SecretKey; + + @RunWith(RobolectricTestRunner.class) public class EncryptedKvBackupTaskTest { private static final boolean INCREMENTAL = true; diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java index 468a97630e19..15920940140c 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -59,6 +59,18 @@ public class IllustrationPreference extends Preference { private Uri mImageUri; private Drawable mImageDrawable; private View mMiddleGroundView; + private OnBindListener mOnBindListener; + + /** + * Interface to listen in on when {@link #onBindViewHolder(PreferenceViewHolder)} occurs. + */ + public interface OnBindListener { + /** + * Called when when {@link #onBindViewHolder(PreferenceViewHolder)} occurs. + * @param animationView the animation view for this preference. + */ + void onBind(LottieAnimationView animationView); + } private final Animatable2.AnimationCallback mAnimationCallback = new Animatable2.AnimationCallback() { @@ -133,6 +145,17 @@ public class IllustrationPreference extends Preference { if (IS_ENABLED_LOTTIE_ADAPTIVE_COLOR) { ColorUtils.applyDynamicColors(getContext(), illustrationView); } + + if (mOnBindListener != null) { + mOnBindListener.onBind(illustrationView); + } + } + + /** + * Sets a listener to be notified when the views are binded. + */ + public void setOnBindListener(OnBindListener listener) { + mOnBindListener = listener; } /** diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java index 1f2297ba3a0c..fc2bf0a9bd93 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java @@ -21,10 +21,10 @@ import static android.os.UserHandle.MU_ENABLED; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyLong; -import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/DataUsageUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/DataUsageUtilsTest.java index 95f7ef41b10b..508dffc2fa21 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/DataUsageUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/net/DataUsageUtilsTest.java @@ -18,7 +18,7 @@ package com.android.settingslib.net; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java index f28572f5f71d..cf07c6bb7cb9 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/EditUserInfoControllerTest.java @@ -22,7 +22,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import android.app.Activity; @@ -143,7 +143,7 @@ public class EditUserInfoControllerTest { dialog.show(); dialog.cancel(); - verifyZeroInteractions(successCallback); + verifyNoInteractions(successCallback); verify(cancelCallback, times(1)) .run(); } @@ -159,7 +159,7 @@ public class EditUserInfoControllerTest { dialog.show(); dialog.getButton(Dialog.BUTTON_NEGATIVE).performClick(); - verifyZeroInteractions(successCallback); + verifyNoInteractions(successCallback); verify(cancelCallback, times(1)) .run(); } @@ -180,7 +180,7 @@ public class EditUserInfoControllerTest { verify(successCallback, times(1)) .accept("test", oldUserIcon); - verifyZeroInteractions(cancelCallback); + verifyNoInteractions(cancelCallback); } @Test @@ -198,7 +198,7 @@ public class EditUserInfoControllerTest { verify(successCallback, times(1)) .accept("test", null); - verifyZeroInteractions(cancelCallback); + verifyNoInteractions(cancelCallback); } @Test @@ -219,7 +219,7 @@ public class EditUserInfoControllerTest { verify(successCallback, times(1)) .accept(expectedNewName, mCurrentIcon); - verifyZeroInteractions(cancelCallback); + verifyNoInteractions(cancelCallback); } @Test @@ -238,7 +238,7 @@ public class EditUserInfoControllerTest { verify(successCallback, times(1)) .accept("test", newPhoto); - verifyZeroInteractions(cancelCallback); + verifyNoInteractions(cancelCallback); } @Test @@ -257,7 +257,7 @@ public class EditUserInfoControllerTest { verify(successCallback, times(1)) .accept("test", newPhoto); - verifyZeroInteractions(cancelCallback); + verifyNoInteractions(cancelCallback); } @Test diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java index 29549d9a7fa7..103512d4a28a 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/IllustrationPreferenceTest.java @@ -61,6 +61,8 @@ public class IllustrationPreferenceTest { private PreferenceViewHolder mViewHolder; private FrameLayout mMiddleGroundLayout; private final Context mContext = ApplicationProvider.getApplicationContext(); + private IllustrationPreference.OnBindListener mOnBindListener; + private LottieAnimationView mOnBindListenerAnimationView; @Before public void setUp() { @@ -82,6 +84,12 @@ public class IllustrationPreferenceTest { final AttributeSet attributeSet = Robolectric.buildAttributeSet().build(); mPreference = new IllustrationPreference(mContext, attributeSet); + mOnBindListener = new IllustrationPreference.OnBindListener() { + @Override + public void onBind(LottieAnimationView animationView) { + mOnBindListenerAnimationView = animationView; + } + }; } @Test @@ -186,4 +194,25 @@ public class IllustrationPreferenceTest { assertThat(mBackgroundView.getMaxHeight()).isEqualTo(restrictedHeight); assertThat(mAnimationView.getMaxHeight()).isEqualTo(restrictedHeight); } + + @Test + public void setOnBindListener_isNotified() { + mOnBindListenerAnimationView = null; + mPreference.setOnBindListener(mOnBindListener); + + mPreference.onBindViewHolder(mViewHolder); + + assertThat(mOnBindListenerAnimationView).isNotNull(); + assertThat(mOnBindListenerAnimationView).isEqualTo(mAnimationView); + } + + @Test + public void setOnBindListener_notNotified() { + mOnBindListenerAnimationView = null; + mPreference.setOnBindListener(null); + + mPreference.onBindViewHolder(mViewHolder); + + assertThat(mOnBindListenerAnimationView).isNull(); + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/UpdatableListPreferenceDialogFragmentTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/UpdatableListPreferenceDialogFragmentTest.java index 0b3495def21f..ca0aa0d7b900 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/UpdatableListPreferenceDialogFragmentTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/UpdatableListPreferenceDialogFragmentTest.java @@ -56,7 +56,7 @@ public class UpdatableListPreferenceDialogFragmentTest { mUpdatableListPrefDlgFragment = spy(UpdatableListPreferenceDialogFragment .newInstance(KEY, MetricsProto.MetricsEvent.DIALOG_SWITCH_A2DP_DEVICES)); - mEntries = spy(new ArrayList<>()); + mEntries = new ArrayList<>(); mUpdatableListPrefDlgFragment.setEntries(mEntries); mUpdatableListPrefDlgFragment .setMetricsCategory(mUpdatableListPrefDlgFragment.getArguments()); diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 1b0b6b4ff796..211030a90c47 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -123,7 +123,7 @@ public class SecureSettings { Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW, Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, - Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, + Settings.Secure.SFPS_PERFORMANT_AUTH_ENABLED, Settings.Secure.ACTIVE_UNLOCK_ON_WAKE, Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT, Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 4fa490ffaed5..0539f09e20d3 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -178,7 +178,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, NON_NEGATIVE_INTEGER_VALIDATOR); - VALIDATORS.put(Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SFPS_PERFORMANT_AUTH_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_MEDIA_WHEN_BYPASSING, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FACE_UNLOCK_APP_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, BOOLEAN_VALIDATOR); diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 810dd33ae17a..75c92e000c4d 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -292,7 +292,7 @@ <queries> <intent> - <action android:name="android.intent.action.NOTES" /> + <action android:name="android.intent.action.CREATE_NOTE" /> </intent> </queries> @@ -411,7 +411,6 @@ <service android:name=".screenshot.ScreenshotCrossProfileService" android:permission="com.android.systemui.permission.SELF" - android:process=":screenshot_cross_profile" android:exported="false" /> <service android:name=".screenrecord.RecordingService" /> diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index 54aa3516d867..a450d3af334b 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -366,7 +366,7 @@ constructor( val dialog = animatedDialog.dialog // Don't animate if the dialog is not showing or if we are locked and going to show the - // bouncer. + // primary bouncer. if ( !dialog.isShowing || (!callback.isUnlocked() && !callback.isShowingAlternateAuthOnUnlock()) diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml index 2b7bdc2dc4cb..c772c9649cc7 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml @@ -27,7 +27,7 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" - androidprv:layout_maxWidth="@dimen/keyguard_security_width" + androidprv:layout_maxWidth="@dimen/biometric_auth_pattern_view_max_size" android:layout_gravity="center_horizontal|bottom" android:clipChildren="false" android:clipToPadding="false"> diff --git a/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml b/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml deleted file mode 100644 index a3c37e420f29..000000000000 --- a/packages/SystemUI/res-keyguard/values-sw540dp-port/dimens.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* //device/apps/common/assets/res/any/dimens.xml -** -** Copyright 2013, 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. -*/ ---> -<resources> - <!-- Height of the sliding KeyguardSecurityContainer - (includes 2x keyguard_security_view_top_margin) --> - <dimen name="keyguard_security_height">550dp</dimen> -</resources> diff --git a/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml index 1dc61c501beb..b7a1bb46c317 100644 --- a/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml +++ b/packages/SystemUI/res-keyguard/values-sw720dp/dimens.xml @@ -17,10 +17,5 @@ */ --> <resources> - - <!-- Height of the sliding KeyguardSecurityContainer - (includes 2x keyguard_security_view_top_margin) --> - <dimen name="keyguard_security_height">470dp</dimen> - <dimen name="widget_big_font_size">100dp</dimen> </resources> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index c5ffdc0051da..6cc5b9d7b7e8 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -29,9 +29,6 @@ (includes 2x keyguard_security_view_top_margin) --> <dimen name="keyguard_security_height">420dp</dimen> - <!-- Max Height of the sliding KeyguardSecurityContainer - (includes 2x keyguard_security_view_top_margin) --> - <!-- pin/password field max height --> <dimen name="keyguard_password_height">80dp</dimen> diff --git a/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml new file mode 100644 index 000000000000..08c5aaf56bf7 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml @@ -0,0 +1,26 @@ +<?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 +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="13dp" + android:height="13dp" + android:viewportWidth="48" + android:viewportHeight="48" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M18.3,34H29.65V31H21.3V25.7H29.65V22.7H21.3V17.35H29.65V14.35H18.3ZM9,42Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6H39Q40.2,6 41.1,6.9Q42,7.8 42,9V39Q42,40.2 41.1,41.1Q40.2,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM9,9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/> +</vector> diff --git a/packages/SystemUI/res/drawable/overlay_badge_background.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml index 857632edcf0d..53122c17e320 100644 --- a/packages/SystemUI/res/drawable/overlay_badge_background.xml +++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ 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,8 +14,11 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:shape="oval"> - <solid android:color="?androidprv:attr/colorSurface"/> -</shape> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:pathData="M0,0M48,48"/> +</vector> diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml index a3dd334bd667..3505a3e6b6bf 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -71,8 +71,8 @@ <com.android.internal.widget.LockPatternView android:id="@+id/lockPattern" android:layout_gravity="center" - android:layout_width="match_parent" - android:layout_height="match_parent"/> + android:layout_width="@dimen/biometric_auth_pattern_view_size" + android:layout_height="@dimen/biometric_auth_pattern_view_size"/> <TextView android:id="@+id/error" diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index 4af997017bba..147ea8221beb 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -67,8 +67,8 @@ <com.android.internal.widget.LockPatternView android:id="@+id/lockPattern" android:layout_gravity="center" - android:layout_width="match_parent" - android:layout_height="match_parent"/> + android:layout_width="@dimen/biometric_auth_pattern_view_size" + android:layout_height="@dimen/biometric_auth_pattern_view_size"/> <TextView android:id="@+id/error" diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml index bc97e511e7f4..8cf4f4de27da 100644 --- a/packages/SystemUI/res/layout/chipbar.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -23,6 +23,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"> + <!-- Extra marginBottom to give room for the drop shadow. --> <LinearLayout android:id="@+id/chipbar_inner" android:orientation="horizontal" @@ -33,6 +34,8 @@ android:layout_marginTop="20dp" android:layout_marginStart="@dimen/notification_side_paddings" android:layout_marginEnd="@dimen/notification_side_paddings" + android:translationZ="4dp" + android:layout_marginBottom="8dp" android:clipToPadding="false" android:gravity="center_vertical" android:alpha="0.0" diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml index 9134f96f59e1..eec3b11519b1 100644 --- a/packages/SystemUI/res/layout/clipboard_overlay.xml +++ b/packages/SystemUI/res/layout/clipboard_overlay.xml @@ -32,26 +32,26 @@ android:elevation="4dp" android:background="@drawable/action_chip_container_background" android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - app:layout_constraintBottom_toBottomOf="@+id/actions_container" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/actions_container" - app:layout_constraintEnd_toEndOf="@+id/actions_container"/> + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toBottomOf="parent"/> <HorizontalScrollView android:id="@+id/actions_container" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" - android:paddingEnd="@dimen/overlay_action_container_padding_right" + android:paddingEnd="@dimen/overlay_action_container_padding_end" android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:scrollbars="none" - android:layout_marginBottom="4dp" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_percent="1.0" app:layout_constraintWidth_max="wrap" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/preview_border" - app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> <LinearLayout android:id="@+id/actions" android:layout_width="wrap_content" @@ -69,44 +69,30 @@ android:id="@+id/preview_border" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="@dimen/overlay_offset_x" - android:layout_marginBottom="12dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginStart="@dimen/overlay_preview_container_margin" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="@dimen/overlay_preview_container_margin" android:elevation="7dp" - app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end" - app:layout_constraintTop_toTopOf="@id/clipboard_preview_top" - android:background="@drawable/overlay_border"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/clipboard_preview_end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierMargin="@dimen/overlay_border_width" - app:barrierDirection="end" - app:constraint_referenced_ids="clipboard_preview"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/clipboard_preview_top" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="top" - app:barrierMargin="@dimen/overlay_border_width_neg" - app:constraint_referenced_ids="clipboard_preview"/> + android:background="@drawable/overlay_border" + app:layout_constraintStart_toStartOf="@id/actions_container_background" + app:layout_constraintTop_toTopOf="@id/clipboard_preview" + app:layout_constraintEnd_toEndOf="@id/clipboard_preview" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/> <FrameLayout android:id="@+id/clipboard_preview" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" + android:layout_gravity="center" android:elevation="7dp" android:background="@drawable/overlay_preview_background" android:clipChildren="true" android:clipToOutline="true" android:clipToPadding="true" - android:layout_width="@dimen/clipboard_preview_size" - android:layout_margin="@dimen/overlay_border_width" - android:layout_height="wrap_content" - android:layout_gravity="center" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintBottom_toBottomOf="@id/preview_border" app:layout_constraintStart_toStartOf="@id/preview_border" - app:layout_constraintEnd_toEndOf="@id/preview_border" - app:layout_constraintTop_toTopOf="@id/preview_border"> + app:layout_constraintBottom_toBottomOf="@id/preview_border"> <TextView android:id="@+id/text_preview" android:textFontWeight="500" android:padding="8dp" diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml index a565988c14ad..d68982876448 100644 --- a/packages/SystemUI/res/layout/combined_qs_header.xml +++ b/packages/SystemUI/res/layout/combined_qs_header.xml @@ -148,9 +148,4 @@ <include layout="@layout/ongoing_privacy_chip"/> </FrameLayout> - <Space - android:layout_width="0dp" - android:layout_height="0dp" - android:id="@+id/space" - /> </com.android.systemui.util.NoRemeasureMotionLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml index 9add32c6ee0a..885e5e2d4441 100644 --- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml +++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml @@ -57,6 +57,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_alarm" android:tint="@android:color/white" android:visibility="gone" @@ -67,6 +68,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_qs_dnd_on" android:tint="@android:color/white" android:visibility="gone" @@ -77,6 +79,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_signal_wifi_off" android:visibility="gone" android:contentDescription="@string/dream_overlay_status_bar_wifi_off" /> diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 95aefab328df..abc83379950a 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -147,6 +147,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" /> + <!-- Explicit Indicator --> + <com.android.internal.widget.CachingIconView + android:id="@+id/media_explicit_indicator" + android:layout_width="@dimen/qs_media_explicit_indicator_icon_size" + android:layout_height="@dimen/qs_media_explicit_indicator_icon_size" + android:src="@drawable/ic_media_explicit_indicator" + /> + <!-- Artist name --> <TextView android:id="@+id/header_artist" diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml index e4e0bd45a2db..496eb6e6130e 100644 --- a/packages/SystemUI/res/layout/screenshot_static.xml +++ b/packages/SystemUI/res/layout/screenshot_static.xml @@ -27,26 +27,26 @@ android:elevation="4dp" android:background="@drawable/action_chip_container_background" android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" - app:layout_constraintBottom_toBottomOf="@+id/actions_container" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/actions_container" - app:layout_constraintEnd_toEndOf="@+id/actions_container"/> + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toTopOf="@id/screenshot_message_container"/> <HorizontalScrollView android:id="@+id/actions_container" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" - android:layout_marginBottom="4dp" - android:paddingEnd="@dimen/overlay_action_container_padding_right" + android:paddingEnd="@dimen/overlay_action_container_padding_end" android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:scrollbars="none" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_percent="1.0" app:layout_constraintWidth_max="wrap" - app:layout_constraintBottom_toTopOf="@id/screenshot_message_container" app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border" - app:layout_constraintEnd_toEndOf="parent"> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> <LinearLayout android:id="@+id/screenshot_actions" android:layout_width="wrap_content" @@ -64,35 +64,24 @@ android:id="@+id/screenshot_preview_border" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="@dimen/overlay_offset_x" - android:layout_marginBottom="12dp" + android:layout_marginStart="@dimen/overlay_preview_container_margin" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="@dimen/overlay_preview_container_margin" android:elevation="7dp" android:alpha="0" android:background="@drawable/overlay_border" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toTopOf="@id/screenshot_message_container" - app:layout_constraintEnd_toEndOf="@id/screenshot_preview_end" - app:layout_constraintTop_toTopOf="@id/screenshot_preview_top"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/screenshot_preview_end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierMargin="@dimen/overlay_border_width" - app:barrierDirection="end" - app:constraint_referenced_ids="screenshot_preview"/> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/screenshot_preview_top" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="top" - app:barrierMargin="@dimen/overlay_border_width_neg" - app:constraint_referenced_ids="screenshot_preview"/> + app:layout_constraintStart_toStartOf="@id/actions_container_background" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/> <ImageView android:id="@+id/screenshot_preview" android:visibility="invisible" android:layout_width="@dimen/overlay_x_scale" - android:layout_margin="@dimen/overlay_border_width" android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" android:layout_gravity="center" android:elevation="7dp" android:contentDescription="@string/screenshot_edit_description" @@ -100,20 +89,14 @@ android:background="@drawable/overlay_preview_background" android:adjustViewBounds="true" android:clickable="true" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" app:layout_constraintStart_toStartOf="@id/screenshot_preview_border" - app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border" - app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/> + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/> <ImageView android:id="@+id/screenshot_badge" - android:layout_width="24dp" - android:layout_height="24dp" - android:padding="4dp" + android:layout_width="48dp" + android:layout_height="48dp" android:visibility="gone" - android:background="@drawable/overlay_badge_background" android:elevation="8dp" - android:src="@drawable/overlay_cancel" app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/> <FrameLayout @@ -150,7 +133,7 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal" android:layout_marginVertical="4dp" - android:paddingHorizontal="@dimen/overlay_action_container_padding_right" + android:paddingHorizontal="@dimen/overlay_action_container_padding_end" android:paddingVertical="@dimen/overlay_action_container_padding_vertical" android:elevation="4dp" android:background="@drawable/action_chip_container_background" diff --git a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml index fa9d7390dcf8..7eaed4356f46 100644 --- a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml +++ b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml @@ -46,7 +46,7 @@ app:layout_constraintEnd_toEndOf="parent" app:flow_horizontalBias="0.5" app:flow_verticalAlign="center" - app:flow_wrapMode="chain" + app:flow_wrapMode="chain2" app:flow_horizontalGap="@dimen/user_switcher_fullscreen_horizontal_gap" app:flow_verticalGap="44dp" app:flow_horizontalStyle="packed"/> diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 49ef330dcc52..fff25448b2e4 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -40,6 +40,10 @@ <dimen name="biometric_dialog_button_negative_max_width">140dp</dimen> <dimen name="biometric_dialog_button_positive_max_width">116dp</dimen> + <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size --> + <dimen name="biometric_auth_pattern_view_size">248dp</dimen> + <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen> + <dimen name="global_actions_power_dialog_item_height">130dp</dimen> <dimen name="global_actions_power_dialog_item_bottom_margin">35dp</dimen> diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml index aefd9981d02e..a0e721e571d8 100644 --- a/packages/SystemUI/res/values-land/styles.xml +++ b/packages/SystemUI/res/values-land/styles.xml @@ -29,11 +29,11 @@ <style name="AuthCredentialPatternContainerStyle"> <item name="android:gravity">center</item> - <item name="android:maxHeight">320dp</item> - <item name="android:maxWidth">320dp</item> - <item name="android:minHeight">200dp</item> - <item name="android:minWidth">200dp</item> - <item name="android:paddingHorizontal">60dp</item> + <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:paddingHorizontal">32dp</item> <item name="android:paddingVertical">20dp</item> </style> diff --git a/packages/SystemUI/res/values-sw360dp/dimens.xml b/packages/SystemUI/res/values-sw360dp/dimens.xml index 65ca70bac0dc..03365b3d8c4f 100644 --- a/packages/SystemUI/res/values-sw360dp/dimens.xml +++ b/packages/SystemUI/res/values-sw360dp/dimens.xml @@ -25,5 +25,8 @@ <!-- Home Controls --> <dimen name="global_actions_side_margin">12dp</dimen> + + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">298dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw392dp-land/dimens.xml b/packages/SystemUI/res/values-sw392dp-land/dimens.xml new file mode 100644 index 000000000000..1e26a699f85a --- /dev/null +++ b/packages/SystemUI/res/values-sw392dp-land/dimens.xml @@ -0,0 +1,21 @@ +<!-- + ~ 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. + --> + +<resources> + <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size --> + <dimen name="biometric_auth_pattern_view_size">248dp</dimen> + <dimen name="biometric_auth_pattern_view_max_size">248dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-sw392dp/dimens.xml b/packages/SystemUI/res/values-sw392dp/dimens.xml index 78279ca4f520..96af3c13f32e 100644 --- a/packages/SystemUI/res/values-sw392dp/dimens.xml +++ b/packages/SystemUI/res/values-sw392dp/dimens.xml @@ -24,5 +24,8 @@ <!-- Home Controls --> <dimen name="global_actions_side_margin">16dp</dimen> + + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">298dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw410dp-land/dimens.xml b/packages/SystemUI/res/values-sw410dp-land/dimens.xml new file mode 100644 index 000000000000..c4d9b9b92f57 --- /dev/null +++ b/packages/SystemUI/res/values-sw410dp-land/dimens.xml @@ -0,0 +1,21 @@ +<!-- + ~ 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. + --> + +<resources> + <!-- Lock pattern view size, align sysui biometric_auth_pattern_view_size --> + <dimen name="biometric_auth_pattern_view_size">248dp</dimen> + <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-sw410dp/dimens.xml b/packages/SystemUI/res/values-sw410dp/dimens.xml index 7da47e5089be..ff6e005a94c7 100644 --- a/packages/SystemUI/res/values-sw410dp/dimens.xml +++ b/packages/SystemUI/res/values-sw410dp/dimens.xml @@ -27,4 +27,6 @@ <dimen name="global_actions_grid_item_side_margin">12dp</dimen> <dimen name="global_actions_grid_item_height">72dp</dimen> + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">348dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml index 8148d3dfaf7d..c535c6416106 100644 --- a/packages/SystemUI/res/values-sw600dp-land/styles.xml +++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml @@ -18,10 +18,10 @@ <style name="AuthCredentialPatternContainerStyle"> <item name="android:gravity">center</item> - <item name="android:maxHeight">420dp</item> - <item name="android:maxWidth">420dp</item> - <item name="android:minHeight">200dp</item> - <item name="android:minWidth">200dp</item> + <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> <item name="android:paddingHorizontal">120dp</item> <item name="android:paddingVertical">40dp</item> </style> diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml index 771de08cb360..32eefa7316b2 100644 --- a/packages/SystemUI/res/values-sw600dp-port/styles.xml +++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml @@ -26,10 +26,10 @@ <style name="AuthCredentialPatternContainerStyle"> <item name="android:gravity">center</item> - <item name="android:maxHeight">420dp</item> - <item name="android:maxWidth">420dp</item> - <item name="android:minHeight">200dp</item> - <item name="android:minWidth">200dp</item> + <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> <item name="android:paddingHorizontal">180dp</item> <item name="android:paddingVertical">80dp</item> </style> diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index 599bf30a5135..9bc0dde49b4c 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -92,4 +92,6 @@ <dimen name="lockscreen_shade_status_bar_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> <dimen name="lockscreen_shade_keyguard_transition_distance">@dimen/lockscreen_shade_media_transition_distance</dimen> + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">348dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml index 3fc59e38ec6c..122806a1af8f 100644 --- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml @@ -27,7 +27,7 @@ <dimen name="status_view_margin_horizontal">24dp</dimen> - <dimen name="qs_media_session_height_expanded">251dp</dimen> + <dimen name="qs_media_session_height_expanded">184dp</dimen> <dimen name="qs_content_horizontal_padding">40dp</dimen> <dimen name="qs_horizontal_margin">40dp</dimen> <!-- in split shade qs_tiles_page_horizontal_margin should be equal of qs_horizontal_margin/2, @@ -36,8 +36,8 @@ <dimen name="qs_tiles_page_horizontal_margin">20dp</dimen> <!-- Size of Smartspace media recommendations cards in the QSPanel carousel --> - <dimen name="qs_media_rec_icon_top_margin">27dp</dimen> - <dimen name="qs_media_rec_album_size">152dp</dimen> + <dimen name="qs_media_rec_icon_top_margin">16dp</dimen> + <dimen name="qs_media_rec_album_size">112dp</dimen> <dimen name="qs_media_rec_album_side_margin">16dp</dimen> <dimen name="lockscreen_shade_max_over_scroll_amount">42dp</dimen> diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml index f9ed67d89de7..6a70ebd07ad2 100644 --- a/packages/SystemUI/res/values-sw720dp-land/styles.xml +++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml @@ -18,10 +18,10 @@ <style name="AuthCredentialPatternContainerStyle"> <item name="android:gravity">center</item> - <item name="android:maxHeight">420dp</item> - <item name="android:maxWidth">420dp</item> - <item name="android:minHeight">200dp</item> - <item name="android:minWidth">200dp</item> + <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> <item name="android:paddingHorizontal">120dp</item> <item name="android:paddingVertical">40dp</item> </style> diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml index 78d299c483e6..0a46e08da22a 100644 --- a/packages/SystemUI/res/values-sw720dp-port/styles.xml +++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml @@ -26,10 +26,10 @@ <style name="AuthCredentialPatternContainerStyle"> <item name="android:gravity">center</item> - <item name="android:maxHeight">420dp</item> - <item name="android:maxWidth">420dp</item> - <item name="android:minHeight">200dp</item> - <item name="android:minWidth">200dp</item> + <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> <item name="android:paddingHorizontal">240dp</item> <item name="android:paddingVertical">120dp</item> </style> diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml index 07050171470a..927059aa7e40 100644 --- a/packages/SystemUI/res/values-sw720dp/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp/dimens.xml @@ -22,5 +22,8 @@ <dimen name="controls_padding_horizontal">75dp</dimen> <dimen name="large_screen_shade_header_height">56dp</dimen> + + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">348dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw800dp/dimens.xml b/packages/SystemUI/res/values-sw800dp/dimens.xml new file mode 100644 index 000000000000..0d82217456e4 --- /dev/null +++ b/packages/SystemUI/res/values-sw800dp/dimens.xml @@ -0,0 +1,24 @@ +<?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. + --> + +<!-- These resources are around just to allow their values to be customized + for different hardware and product builds. --> +<resources> + + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">348dp</dimen> +</resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 077ef0f16fe2..e8a5e7ed8546 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -668,6 +668,16 @@ <item>17</item> <!-- WAKE_REASON_BIOMETRIC --> </integer-array> + <!-- Whether to support posture listening for face auth, default is 0(DEVICE_POSTURE_UNKNOWN) + means systemui will try listening on all postures. + 0 : DEVICE_POSTURE_UNKNOWN + 1 : DEVICE_POSTURE_CLOSED + 2 : DEVICE_POSTURE_HALF_OPENED + 3 : DEVICE_POSTURE_OPENED + 4 : DEVICE_POSTURE_FLIPPED + --> + <integer name="config_face_auth_supported_posture">0</integer> + <!-- Whether the communal service should be enabled --> <bool name="config_communalServiceEnabled">false</bool> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 6841bf897e92..890d96444b04 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -334,15 +334,22 @@ <dimen name="overlay_action_chip_spacing">8dp</dimen> <dimen name="overlay_action_chip_text_size">14sp</dimen> <dimen name="overlay_offset_x">16dp</dimen> + <!-- Used for both start and bottom margin of the preview, relative to the action container --> + <dimen name="overlay_preview_container_margin">8dp</dimen> <dimen name="overlay_action_container_margin_horizontal">8dp</dimen> + <dimen name="overlay_action_container_margin_bottom">4dp</dimen> <dimen name="overlay_bg_protection_height">242dp</dimen> <dimen name="overlay_action_container_corner_radius">18dp</dimen> <dimen name="overlay_action_container_padding_vertical">4dp</dimen> <dimen name="overlay_action_container_padding_right">8dp</dimen> + <dimen name="overlay_action_container_padding_end">8dp</dimen> <dimen name="overlay_dismiss_button_tappable_size">48dp</dimen> <dimen name="overlay_dismiss_button_margin">8dp</dimen> + <!-- must be kept aligned with overlay_border_width_neg, below; + overlay_border_width = overlay_border_width_neg * -1 --> <dimen name="overlay_border_width">4dp</dimen> - <!-- need a negative margin for some of the constraints. should be overlay_border_width * -1 --> + <!-- some constraints use a negative margin. must be aligned with overlay_border_width, above; + overlay_border_width_neg = overlay_border_width * -1 --> <dimen name="overlay_border_width_neg">-4dp</dimen> <dimen name="clipboard_preview_size">@dimen/overlay_x_scale</dimen> @@ -966,6 +973,10 @@ <!-- Biometric Auth Credential values --> <dimen name="biometric_auth_icon_size">48dp</dimen> + <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> + <dimen name="biometric_auth_pattern_view_size">348dp</dimen> + <dimen name="biometric_auth_pattern_view_max_size">348dp</dimen> + <!-- Starting text size in sp of batteryLevel for wireless charging animation --> <item name="wireless_charging_anim_battery_level_text_size_start" format="float" type="dimen"> 0 @@ -1030,8 +1041,6 @@ <dimen name="ongoing_appops_dialog_side_padding">16dp</dimen> - <!-- Size of the RAT type for CellularTile --> - <!-- Size of media cards in the QSPanel carousel --> <dimen name="qs_media_padding">16dp</dimen> <dimen name="qs_media_album_radius">14dp</dimen> @@ -1046,6 +1055,7 @@ <dimen name="qs_media_disabled_seekbar_height">1dp</dimen> <dimen name="qs_media_enabled_seekbar_height">2dp</dimen> <dimen name="qs_media_app_icon_size">24dp</dimen> + <dimen name="qs_media_explicit_indicator_icon_size">13dp</dimen> <dimen name="qs_media_session_enabled_seekbar_vertical_padding">15dp</dimen> <dimen name="qs_media_session_disabled_seekbar_vertical_padding">16dp</dimen> @@ -1282,6 +1292,9 @@ <!-- LOCKSCREEN -> DREAMING transition: Amount to shift lockscreen content on entering --> <dimen name="lockscreen_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen> + <!-- GONE -> DREAMING transition: Amount to shift lockscreen content on entering --> + <dimen name="gone_to_dreaming_transition_lockscreen_translation_y">-40dp</dimen> + <!-- LOCKSCREEN -> OCCLUDED transition: Amount to shift lockscreen content on entering --> <dimen name="lockscreen_to_occluded_transition_lockscreen_translation_y">-40dp</dimen> @@ -1623,6 +1636,8 @@ <dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen> + <dimen name="dream_overlay_icon_inset_dimen">0dp</dimen> + <dimen name="dream_overlay_status_bar_marginTop">22dp</dimen> <!-- Default device corner radius, used for assist UI --> <dimen name="config_rounded_mask_size">0px</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index db338b63da78..2de16a417d20 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2359,13 +2359,15 @@ <!-- Text to ask the user to move their device closer to a different device (deviceName) in order to play media on the different device. [CHAR LIMIT=75] --> <string name="media_move_closer_to_start_cast">Move closer to play on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string> <!-- Text to ask the user to move their device closer to a different device (deviceName) in order to transfer media from the different device and back onto the current device. [CHAR LIMIT=75] --> - <string name="media_move_closer_to_end_cast">Move closer to <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g> to play here</string> + <string name="media_move_closer_to_end_cast">To play here, move closer to <xliff:g id="deviceName" example="tablet">%1$s</xliff:g></string> <!-- Text informing the user that their media is now playing on a different device (deviceName). [CHAR LIMIT=50] --> <string name="media_transfer_playing_different_device">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string> - <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIsMIT=50] --> + <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIMIT=50] --> <string name="media_transfer_failed">Something went wrong. Try again.</string> <!-- Text to indicate that a media transfer is currently in-progress, aka loading. [CHAR LIMIT=NONE] --> <string name="media_transfer_loading">Loading</string> + <!-- Default name of the device. [CHAR LIMIT=30] --> + <string name="media_ttt_default_device_type">tablet</string> <!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] --> <string name="controls_error_timeout">Inactive, check app</string> @@ -2770,6 +2772,9 @@ <!-- Text for education page content description for unfolded animation. [CHAR_LIMIT=NONE] --> <string name="rear_display_accessibility_unfolded_animation">Foldable device being flipped around</string> - <!-- Title for notification of low stylus battery. [CHAR_LIMIT=NONE] --> - <string name="stylus_battery_low">Stylus battery low</string> + <!-- Title for notification of low stylus battery with percentage. "percentage" is + the value of the battery capacity remaining [CHAR LIMIT=none]--> + <string name="stylus_battery_low_percentage"><xliff:g id="percentage" example="16%">%s</xliff:g> battery remaining</string> + <!-- Subtitle for the notification sent when a stylus battery is low. [CHAR LIMIT=none]--> + <string name="stylus_battery_low_subtitle">Connect your stylus to a charger</string> </resources> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index b11b6d633f14..9846fc251a27 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -251,11 +251,12 @@ <style name="AuthCredentialPatternContainerStyle"> <item name="android:gravity">center</item> - <item name="android:maxHeight">420dp</item> - <item name="android:maxWidth">420dp</item> - <item name="android:minHeight">200dp</item> - <item name="android:minWidth">200dp</item> - <item name="android:padding">20dp</item> + <item name="android:maxHeight">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:maxWidth">@dimen/biometric_auth_pattern_view_max_size</item> + <item name="android:minHeight">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:minWidth">@dimen/biometric_auth_pattern_view_size</item> + <item name="android:paddingHorizontal">32dp</item> + <item name="android:paddingVertical">20dp</item> </style> <style name="AuthCredentialPinPasswordContainerStyle"> diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml index 1eb621e0368b..d9c81af54a12 100644 --- a/packages/SystemUI/res/xml/media_session_collapsed.xml +++ b/packages/SystemUI/res/xml/media_session_collapsed.xml @@ -66,6 +66,21 @@ app:layout_constraintTop_toBottomOf="@id/icon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintHorizontal_bias="0" /> + + <Constraint + android:id="@+id/media_explicit_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_info_spacing" + android:layout_marginBottom="@dimen/qs_media_padding" + android:layout_marginTop="0dp" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toStartOf="@id/header_artist" + app:layout_constraintTop_toTopOf="@id/header_artist" + app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="packed" /> + <Constraint android:id="@+id/header_artist" android:layout_width="wrap_content" @@ -75,9 +90,8 @@ app:layout_constraintEnd_toStartOf="@id/action_button_guideline" app:layout_constrainedWidth="true" app:layout_constraintTop_toBottomOf="@id/header_title" - app:layout_constraintStart_toStartOf="@id/header_title" - app:layout_constraintVertical_bias="0" - app:layout_constraintHorizontal_bias="0" /> + app:layout_constraintStart_toEndOf="@id/media_explicit_indicator" + app:layout_constraintVertical_bias="0" /> <Constraint android:id="@+id/actionPlayPause" diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml index 7de0a5e0e8c4..0cdc0f9505bc 100644 --- a/packages/SystemUI/res/xml/media_session_expanded.xml +++ b/packages/SystemUI/res/xml/media_session_expanded.xml @@ -58,6 +58,21 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toTopOf="@id/header_artist" app:layout_constraintHorizontal_bias="0" /> + + <Constraint + android:id="@+id/media_explicit_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_info_spacing" + android:layout_marginBottom="@dimen/qs_media_padding" + android:layout_marginTop="0dp" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toStartOf="@id/header_artist" + app:layout_constraintTop_toTopOf="@id/header_artist" + app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="packed"/> + <Constraint android:id="@+id/header_artist" android:layout_width="wrap_content" @@ -67,10 +82,9 @@ android:layout_marginTop="0dp" app:layout_constrainedWidth="true" app:layout_constraintEnd_toStartOf="@id/actionPlayPause" - app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintStart_toEndOf="@id/media_explicit_indicator" app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" - app:layout_constraintVertical_bias="0" - app:layout_constraintHorizontal_bias="0" /> + app:layout_constraintVertical_bias="0" /> <Constraint android:id="@+id/actionPlayPause" diff --git a/packages/SystemUI/res/xml/qs_header.xml b/packages/SystemUI/res/xml/qs_header.xml index eca2b2acb079..d97031f35d6b 100644 --- a/packages/SystemUI/res/xml/qs_header.xml +++ b/packages/SystemUI/res/xml/qs_header.xml @@ -56,13 +56,9 @@ <Layout android:layout_width="wrap_content" android:layout_height="@dimen/new_qs_header_non_clickable_element_height" - app:layout_constrainedWidth="true" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/space" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/carrier_group" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintHorizontal_chainStyle="spread_inside" /> </Constraint> @@ -87,39 +83,27 @@ <Constraint android:id="@+id/statusIcons"> <Layout - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="@dimen/new_qs_header_non_clickable_element_height" - app:layout_constraintStart_toEndOf="@id/space" + app:layout_constraintWidth_default="wrap" + app:layout_constraintStart_toEndOf="@id/date" app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon" app:layout_constraintTop_toTopOf="@id/date" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintHorizontal_bias="1" + app:layout_constraintBottom_toBottomOf="@id/date" /> </Constraint> <Constraint android:id="@+id/batteryRemainingIcon"> <Layout - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="@dimen/new_qs_header_non_clickable_element_height" + app:layout_constraintWidth_default="wrap" app:layout_constraintHeight_min="@dimen/new_qs_header_non_clickable_element_height" - app:layout_constraintStart_toEndOf="@id/statusIcons" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/date" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintHorizontal_bias="1" - app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintBottom_toBottomOf="@id/date" /> </Constraint> - - <Constraint - android:id="@id/space"> - <Layout - android:layout_width="0dp" - android:layout_height="0dp" - app:layout_constraintStart_toEndOf="@id/date" - app:layout_constraintEnd_toStartOf="@id/statusIcons" - /> - </Constraint> </ConstraintSet>
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt index 25d272185bc0..9b73cc3ea9f8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt @@ -48,48 +48,28 @@ constructor( val drawableInsetSize: Int try { val keyShadowBlur = - attributes.getDimensionPixelSize(R.styleable.DoubleShadowTextView_keyShadowBlur, 0) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowBlur, 0f) val keyShadowOffsetX = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_keyShadowOffsetX, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetX, 0f) val keyShadowOffsetY = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_keyShadowOffsetY, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetY, 0f) val keyShadowAlpha = attributes.getFloat(R.styleable.DoubleShadowTextView_keyShadowAlpha, 0f) mKeyShadowInfo = - ShadowInfo( - keyShadowBlur.toFloat(), - keyShadowOffsetX.toFloat(), - keyShadowOffsetY.toFloat(), - keyShadowAlpha - ) + ShadowInfo(keyShadowBlur, keyShadowOffsetX, keyShadowOffsetY, keyShadowAlpha) val ambientShadowBlur = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowBlur, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowBlur, 0f) val ambientShadowOffsetX = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowOffsetX, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetX, 0f) val ambientShadowOffsetY = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowOffsetY, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetY, 0f) val ambientShadowAlpha = attributes.getFloat(R.styleable.DoubleShadowTextView_ambientShadowAlpha, 0f) mAmbientShadowInfo = ShadowInfo( - ambientShadowBlur.toFloat(), - ambientShadowOffsetX.toFloat(), - ambientShadowOffsetY.toFloat(), + ambientShadowBlur, + ambientShadowOffsetX, + ambientShadowOffsetY, ambientShadowAlpha ) drawableSize = diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index fd41cb0630dd..6bfaf5e49820 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -283,17 +283,6 @@ public class ActivityManagerWrapper { } /** - * @return whether screen pinning is active. - */ - public boolean isScreenPinningActive() { - try { - return getService().getLockTaskModeState() == LOCK_TASK_MODE_PINNED; - } catch (RemoteException e) { - return false; - } - } - - /** * @return whether screen pinning is enabled. */ public boolean isScreenPinningEnabled() { diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java index 8af934f66b2a..dd52cfbdc80f 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java @@ -16,6 +16,7 @@ package com.android.systemui.shared.system; +import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; import android.app.TaskStackListener; @@ -27,6 +28,8 @@ import android.os.Trace; import android.util.Log; import android.window.TaskSnapshot; +import androidx.annotation.VisibleForTesting; + import com.android.internal.os.SomeArgs; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -43,15 +46,51 @@ public class TaskStackChangeListeners { private final Impl mImpl; + /** + * Proxies calls to the given handler callback synchronously for testing purposes. + */ + private static class TestSyncHandler extends Handler { + private Handler.Callback mCb; + + public TestSyncHandler() { + super(Looper.getMainLooper()); + } + + public void setCallback(Handler.Callback cb) { + mCb = cb; + } + + @Override + public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) { + return mCb.handleMessage(msg); + } + } + private TaskStackChangeListeners() { mImpl = new Impl(Looper.getMainLooper()); } + private TaskStackChangeListeners(Handler h) { + mImpl = new Impl(h); + } + public static TaskStackChangeListeners getInstance() { return INSTANCE; } /** + * Returns an instance of the listeners that can be called upon synchronously for testsing + * purposes. + */ + @VisibleForTesting + public static TaskStackChangeListeners getTestInstance() { + TestSyncHandler h = new TestSyncHandler(); + TaskStackChangeListeners l = new TaskStackChangeListeners(h); + h.setCallback(l.mImpl); + return l; + } + + /** * Registers a task stack listener with the system. * This should be called on the main thread. */ @@ -71,7 +110,15 @@ public class TaskStackChangeListeners { } } - private static class Impl extends TaskStackListener implements Handler.Callback { + /** + * Returns an instance of the listener to call upon from tests. + */ + @VisibleForTesting + public TaskStackListener getListenerImpl() { + return mImpl; + } + + private class Impl extends TaskStackListener implements Handler.Callback { private static final int ON_TASK_STACK_CHANGED = 1; private static final int ON_TASK_SNAPSHOT_CHANGED = 2; @@ -104,10 +151,14 @@ public class TaskStackChangeListeners { private final Handler mHandler; private boolean mRegistered; - Impl(Looper looper) { + private Impl(Looper looper) { mHandler = new Handler(looper, this); } + private Impl(Handler handler) { + mHandler = handler; + } + public void addListener(TaskStackChangeListener listener) { synchronized (mTaskStackListeners) { mTaskStackListeners.add(listener); diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt index 5bb9367fa4a5..e0cf7b6a2bc4 100644 --- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt +++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt @@ -50,6 +50,7 @@ import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_RESET import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_VISIBILITY_CHANGED import com.android.keyguard.InternalFaceAuthReasons.NON_STRONG_BIOMETRIC_ALLOWED_CHANGED import com.android.keyguard.InternalFaceAuthReasons.OCCLUDING_APP_REQUESTED +import com.android.keyguard.InternalFaceAuthReasons.POSTURE_CHANGED import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN import com.android.keyguard.InternalFaceAuthReasons.RETRY_AFTER_HW_UNAVAILABLE @@ -126,6 +127,7 @@ private object InternalFaceAuthReasons { const val STRONG_AUTH_ALLOWED_CHANGED = "Face auth stopped because strong auth allowed changed" const val NON_STRONG_BIOMETRIC_ALLOWED_CHANGED = "Face auth stopped because non strong biometric allowed changed" + const val POSTURE_CHANGED = "Face auth started/stopped due to device posture changed." } /** @@ -173,6 +175,7 @@ constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) : return PowerManager.wakeReasonToString(extraInfo) } }, + @UiEvent(doc = POSTURE_CHANGED) FACE_AUTH_UPDATED_POSTURE_CHANGED(1265, POSTURE_CHANGED), @Deprecated( "Not a face auth trigger.", ReplaceWith( diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 0326b6d3edca..4acbb0aaf1d8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -209,6 +209,7 @@ public class KeyguardClockSwitch extends RelativeLayout { if (!animate) { out.setAlpha(0f); + out.setVisibility(INVISIBLE); in.setAlpha(1f); in.setVisibility(VISIBLE); mStatusArea.setTranslationY(statusAreaYTranslation); @@ -224,7 +225,10 @@ public class KeyguardClockSwitch extends RelativeLayout { direction * -mClockSwitchYAmount)); mClockOutAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mClockOutAnim = null; + if (mClockOutAnim == animation) { + out.setVisibility(INVISIBLE); + mClockOutAnim = null; + } } }); @@ -238,7 +242,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mClockInAnim.setStartDelay(CLOCK_OUT_MILLIS / 2); mClockInAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mClockInAnim = null; + if (mClockInAnim == animation) { + mClockInAnim = null; + } } }); @@ -251,7 +257,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); mStatusAreaAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mStatusAreaAnim = null; + if (mStatusAreaAnim == animation) { + mStatusAreaAnim = null; + } } }); mStatusAreaAnim.start(); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 8c4fa886de47..88ce2a74c99d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -346,10 +346,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS int clockHeight = clock.getLargeClock().getView().getHeight(); return frameHeight / 2 + clockHeight / 2 + mKeyguardLargeClockTopMargin / -2; } else { - // This is only called if we've never shown the large clock as the frame is inflated - // with 'gone', but then the visibility is never set when it is animated away by - // KeyguardClockSwitch, instead it is removed from the view hierarchy. - // TODO(b/261755021): Cleanup Large Frame Visibility int clockHeight = clock.getSmallClock().getView().getHeight(); return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin; } @@ -367,15 +363,11 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS if (mLargeClockFrame.getVisibility() == View.VISIBLE) { return clock.getLargeClock().getView().getHeight(); } else { - // Is not called except in certain edge cases, see comment in getClockBottom - // TODO(b/261755021): Cleanup Large Frame Visibility return clock.getSmallClock().getView().getHeight(); } } boolean isClockTopAligned() { - // Returns false except certain edge cases, see comment in getClockBottom - // TODO(b/261755021): Cleanup Large Frame Visibility return mLargeClockFrame.getVisibility() != View.VISIBLE; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt index deead1959b8a..1a06b5f1c767 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt @@ -39,6 +39,7 @@ data class KeyguardFaceListenModel( var keyguardGoingAway: Boolean = false, var listeningForFaceAssistant: Boolean = false, var occludingAppRequestingFaceAuth: Boolean = false, + val postureAllowsListening: Boolean = false, var primaryUser: Boolean = false, var secureCameraLaunched: Boolean = false, var supportsDetect: Boolean = false, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 93027c1914ee..204f09ee4678 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -63,11 +63,13 @@ import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_RE import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_KEYGUARD_INIT; +import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_POSTURE_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING; import static com.android.systemui.DejankUtils.whitelistIpcs; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import android.annotation.AnyThread; import android.annotation.MainThread; @@ -141,6 +143,7 @@ import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -154,6 +157,7 @@ import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; +import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.Assert; import com.android.systemui.util.settings.SecureSettings; @@ -170,6 +174,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.Executor; @@ -345,18 +350,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>> mCallbacks = Lists.newArrayList(); private ContentObserver mDeviceProvisionedObserver; - private ContentObserver mSfpsRequireScreenOnToAuthPrefObserver; private final ContentObserver mTimeFormatChangeObserver; private boolean mSwitchingUser; private boolean mDeviceInteractive; - private boolean mSfpsRequireScreenOnToAuthPrefEnabled; private final SubscriptionManager mSubscriptionManager; private final TelephonyListenerManager mTelephonyListenerManager; private final TrustManager mTrustManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; + private final DevicePostureController mPostureController; private final BroadcastDispatcher mBroadcastDispatcher; private final SecureSettings mSecureSettings; private final InteractionJankMonitor mInteractionJankMonitor; @@ -374,6 +378,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final FaceManager mFaceManager; private final LockPatternUtils mLockPatternUtils; private final boolean mWakeOnFingerprintAcquiredStart; + @VisibleForTesting + @DevicePostureController.DevicePostureInt + protected int mConfigFaceAuthSupportedPosture; private KeyguardBypassController mKeyguardBypassController; private List<SubscriptionInfo> mSubscriptionInfo; @@ -384,6 +391,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private boolean mLogoutEnabled; private boolean mIsFaceEnrolled; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + private int mPostureState = DEVICE_POSTURE_UNKNOWN; + private FingerprintInteractiveToAuthProvider mFingerprintInteractiveToAuthProvider; /** * Short delay before restarting fingerprint authentication after a successful try. This should @@ -711,8 +720,18 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab */ public void setKeyguardGoingAway(boolean goingAway) { mKeyguardGoingAway = goingAway; - // This is set specifically to stop face authentication from running. - updateBiometricListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY); + if (mKeyguardGoingAway) { + updateFaceListeningState(BIOMETRIC_ACTION_STOP, + FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY); + } + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); + } + + /** + * Whether keyguard is going away due to screen off or device entry. + */ + public boolean isKeyguardGoingAway() { + return mKeyguardGoingAway; } /** @@ -1784,6 +1803,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab }; @VisibleForTesting + final DevicePostureController.Callback mPostureCallback = + new DevicePostureController.Callback() { + @Override + public void onPostureChanged(int posture) { + mPostureState = posture; + updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_UPDATED_POSTURE_CHANGED); + } + }; + + @VisibleForTesting CancellationSignal mFingerprintCancelSignal; @VisibleForTesting CancellationSignal mFaceCancelSignal; @@ -1943,9 +1973,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab cb.onFinishedGoingToSleep(arg1); } } - // This is set specifically to stop face authentication from running. - updateBiometricListeningState(BIOMETRIC_ACTION_STOP, + updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP); + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } private void handleScreenTurnedOff() { @@ -2048,7 +2078,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Nullable FaceManager faceManager, @Nullable FingerprintManager fingerprintManager, @Nullable BiometricManager biometricManager, - FaceWakeUpTriggersConfig faceWakeUpTriggersConfig) { + FaceWakeUpTriggersConfig faceWakeUpTriggersConfig, + DevicePostureController devicePostureController, + Optional<FingerprintInteractiveToAuthProvider> interactiveToAuthProvider) { mContext = context; mSubscriptionManager = subscriptionManager; mUserTracker = userTracker; @@ -2077,6 +2109,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mDreamManager = dreamManager; mTelephonyManager = telephonyManager; mDevicePolicyManager = devicePolicyManager; + mPostureController = devicePostureController; mPackageManager = packageManager; mFpm = fingerprintManager; mFaceManager = faceManager; @@ -2088,6 +2121,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab R.array.config_face_acquire_device_entry_ignorelist)) .boxed() .collect(Collectors.toSet()); + mConfigFaceAuthSupportedPosture = mContext.getResources().getInteger( + R.integer.config_face_auth_supported_posture); mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig; mHandler = new Handler(mainLooper) { @@ -2278,6 +2313,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED)); } }); + if (mConfigFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) { + mPostureController.addCallback(mPostureCallback); + } updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ON_KEYGUARD_INIT); TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); @@ -2311,30 +2349,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Settings.System.getUriFor(Settings.System.TIME_12_24), false, mTimeFormatChangeObserver, UserHandle.USER_ALL); - updateSfpsRequireScreenOnToAuthPref(); - mSfpsRequireScreenOnToAuthPrefObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange) { - updateSfpsRequireScreenOnToAuthPref(); - } - }; - - mContext.getContentResolver().registerContentObserver( - mSecureSettings.getUriFor( - Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED), - false, - mSfpsRequireScreenOnToAuthPrefObserver, - getCurrentUser()); - } - - protected void updateSfpsRequireScreenOnToAuthPref() { - final int defaultSfpsRequireScreenOnToAuthValue = - mContext.getResources().getBoolean( - com.android.internal.R.bool.config_requireScreenOnToAuthEnabled) ? 1 : 0; - mSfpsRequireScreenOnToAuthPrefEnabled = mSecureSettings.getIntForUser( - Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, - defaultSfpsRequireScreenOnToAuthValue, - getCurrentUser()) != 0; + mFingerprintInteractiveToAuthProvider = interactiveToAuthProvider.orElse(null); } private void initializeSimState() { @@ -2729,8 +2744,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab boolean shouldListenSideFpsState = true; if (isSideFps) { + final boolean interactiveToAuthEnabled = + mFingerprintInteractiveToAuthProvider != null && + mFingerprintInteractiveToAuthProvider.isEnabled(getCurrentUser()); shouldListenSideFpsState = - mSfpsRequireScreenOnToAuthPrefEnabled ? isDeviceInteractive() : true; + interactiveToAuthEnabled ? isDeviceInteractive() && !mGoingToSleep : true; } boolean shouldListen = shouldListenKeyguardState && shouldListenUserState @@ -2742,7 +2760,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab user, shouldListen, biometricEnabledForUser, - mPrimaryBouncerIsOrWillBeShowing, + mPrimaryBouncerIsOrWillBeShowing, userCanSkipBouncer, mCredentialAttempted, mDeviceInteractive, @@ -2802,6 +2820,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user); final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant(); final boolean isUdfpsFingerDown = mAuthController.isUdfpsFingerDown(); + final boolean isPostureAllowedForFaceAuth = + mConfigFaceAuthSupportedPosture == 0 /* DEVICE_POSTURE_UNKNOWN */ ? true + : (mPostureState == mConfigFaceAuthSupportedPosture); // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware. @@ -2818,7 +2839,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && faceAuthAllowedOrDetectionIsNeeded && mIsPrimaryUser && (!mSecureCameraLaunched || mOccludingAppRequestingFace) && faceAndFpNotAuthenticated - && !mGoingToSleep; + && !mGoingToSleep + && isPostureAllowedForFaceAuth; // Aggregate relevant fields for debug logging. logListenerModelData( @@ -2838,6 +2860,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mKeyguardGoingAway, shouldListenForFaceAssistant, mOccludingAppRequestingFace, + isPostureAllowedForFaceAuth, mIsPrimaryUser, mSecureCameraLaunched, supportsDetect, @@ -2923,7 +2946,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab getKeyguardSessionId(), faceAuthUiEvent.getExtraInfo() ); - + mLogger.logFaceUnlockPossible(unlockPossible); if (unlockPossible) { mFaceCancelSignal = new CancellationSignal(); @@ -3845,11 +3868,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver); } - if (mSfpsRequireScreenOnToAuthPrefObserver != null) { - mContext.getContentResolver().unregisterContentObserver( - mSfpsRequireScreenOnToAuthPrefObserver); - } - try { ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver); } catch (RemoteException e) { @@ -3926,8 +3944,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } else if (isSfpsSupported()) { pw.println(" sfpsEnrolled=" + isSfpsEnrolled()); pw.println(" shouldListenForSfps=" + shouldListenForFingerprint(false)); - pw.println(" mSfpsRequireScreenOnToAuthPrefEnabled=" - + mSfpsRequireScreenOnToAuthPrefEnabled); + if (isSfpsEnrolled()) { + final boolean interactiveToAuthEnabled = + mFingerprintInteractiveToAuthProvider != null && + mFingerprintInteractiveToAuthProvider + .isEnabled(getCurrentUser()); + pw.println(" interactiveToAuthEnabled=" + + interactiveToAuthEnabled); + } } new DumpsysTableLogger( "KeyguardFingerprintListen", diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 21d3b24174b6..5b4245595be9 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -132,6 +132,12 @@ class KeyguardUpdateMonitorLogger @Inject constructor( logBuffer.log(TAG, DEBUG, { int1 = faceRunningState }, { "faceRunningState: $int1" }) } + fun logFaceUnlockPossible(isFaceUnlockPossible: Boolean) { + logBuffer.log(TAG, DEBUG, + { bool1 = isFaceUnlockPossible }, + {"isUnlockWithFacePossible: $bool1"}) + } + fun logFingerprintAuthForWrongUser(authUserId: Int) { logBuffer.log(TAG, DEBUG, { int1 = authUserId }, diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java index 0fc9ef96f6e9..632fcdc16259 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java @@ -22,8 +22,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.util.Log; -import androidx.annotation.Nullable; - import com.android.systemui.dagger.GlobalRootComponent; import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dagger.WMComponent; @@ -55,7 +53,6 @@ public abstract class SystemUIInitializer { mContext = context; } - @Nullable protected abstract GlobalRootComponent.Builder getGlobalRootComponentBuilder(); /** @@ -72,11 +69,6 @@ public abstract class SystemUIInitializer { * Starts the initialization process. This stands up the Dagger graph. */ public void init(boolean fromTest) throws ExecutionException, InterruptedException { - GlobalRootComponent.Builder globalBuilder = getGlobalRootComponentBuilder(); - if (globalBuilder == null) { - return; - } - mRootComponent = getGlobalRootComponentBuilder() .context(mContext) .instrumentationTest(fromTest) @@ -127,7 +119,6 @@ public abstract class SystemUIInitializer { .setBackAnimation(Optional.ofNullable(null)) .setDesktopMode(Optional.ofNullable(null)); } - mSysUIComponent = builder.build(); if (initializeComponents) { mSysUIComponent.init(); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt index 55c095b0be25..8aa3040c6015 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui -import android.app.Application import android.content.Context import com.android.systemui.dagger.DaggerReferenceGlobalRootComponent import com.android.systemui.dagger.GlobalRootComponent @@ -25,17 +24,7 @@ import com.android.systemui.dagger.GlobalRootComponent * {@link SystemUIInitializer} that stands up AOSP SystemUI. */ class SystemUIInitializerImpl(context: Context) : SystemUIInitializer(context) { - - override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder? { - return when (Application.getProcessName()) { - SCREENSHOT_CROSS_PROFILE_PROCESS -> null - else -> DaggerReferenceGlobalRootComponent.builder() - } - } - - companion object { - private const val SYSTEMUI_PROCESS = "com.android.systemui" - private const val SCREENSHOT_CROSS_PROFILE_PROCESS = - "$SYSTEMUI_PROCESS:screenshot_cross_profile" + override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder { + return DaggerReferenceGlobalRootComponent.builder() } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 092339a74e3f..2dc0cd34adf4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -18,6 +18,7 @@ package com.android.systemui.biometrics; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; +import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP; @@ -76,6 +77,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeReceiver; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.data.repository.BiometricType; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.VibratorHelper; @@ -85,8 +87,10 @@ import com.android.systemui.util.concurrency.Execution; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -150,6 +154,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @Nullable private List<FingerprintSensorPropertiesInternal> mUdfpsProps; @Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps; + @NonNull private final Map<Integer, Boolean> mFpEnrolledForUser = new HashMap<>(); @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser; @NonNull private final SparseBooleanArray mSfpsEnrolledForUser; @NonNull private final SensorPrivacyManager mSensorPrivacyManager; @@ -161,7 +166,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, private final @Background DelayableExecutor mBackgroundExecutor; private final DisplayInfo mCachedDisplayInfo = new DisplayInfo(); - private final VibratorHelper mVibratorHelper; private void vibrateSuccess(int modality) { @@ -331,27 +335,35 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mExecution.assertIsMainThread(); Log.d(TAG, "handleEnrollmentsChanged, userId: " + userId + ", sensorId: " + sensorId + ", hasEnrollments: " + hasEnrollments); - if (mUdfpsProps == null) { - Log.d(TAG, "handleEnrollmentsChanged, mUdfpsProps is null"); - } else { - for (FingerprintSensorPropertiesInternal prop : mUdfpsProps) { + BiometricType sensorBiometricType = BiometricType.UNKNOWN; + if (mFpProps != null) { + for (FingerprintSensorPropertiesInternal prop: mFpProps) { if (prop.sensorId == sensorId) { - mUdfpsEnrolledForUser.put(userId, hasEnrollments); + mFpEnrolledForUser.put(userId, hasEnrollments); + if (prop.isAnyUdfpsType()) { + sensorBiometricType = BiometricType.UNDER_DISPLAY_FINGERPRINT; + mUdfpsEnrolledForUser.put(userId, hasEnrollments); + } else if (prop.isAnySidefpsType()) { + sensorBiometricType = BiometricType.SIDE_FINGERPRINT; + mSfpsEnrolledForUser.put(userId, hasEnrollments); + } else if (prop.sensorType == TYPE_REAR) { + sensorBiometricType = BiometricType.REAR_FINGERPRINT; + } + break; } } } - - if (mSidefpsProps == null) { - Log.d(TAG, "handleEnrollmentsChanged, mSidefpsProps is null"); - } else { - for (FingerprintSensorPropertiesInternal prop : mSidefpsProps) { + if (mFaceProps != null && sensorBiometricType == BiometricType.UNKNOWN) { + for (FaceSensorPropertiesInternal prop : mFaceProps) { if (prop.sensorId == sensorId) { - mSfpsEnrolledForUser.put(userId, hasEnrollments); + sensorBiometricType = BiometricType.FACE; + break; } } } for (Callback cb : mCallbacks) { cb.onEnrollmentsChanged(); + cb.onEnrollmentsChanged(sensorBiometricType, userId, hasEnrollments); } } @@ -604,6 +616,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } } + /** Get FP sensor properties */ + public @Nullable List<FingerprintSensorPropertiesInternal> getFingerprintProperties() { + return mFpProps; + } + /** * @return where the face sensor exists in pixels in the current device orientation. Returns * null if no face sensor exists. @@ -828,7 +845,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } @Override - public void setBiometicContextListener(IBiometricContextListener listener) { + public void setBiometricContextListener(IBiometricContextListener listener) { mBiometricContextListener = listener; notifyDozeChanged(mStatusBarStateController.isDozing(), mWakefulnessLifecycle.getWakefulness()); @@ -1081,6 +1098,13 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return mSfpsEnrolledForUser.get(userId); } + /** + * Whether the passed userId has enrolled at least one fingerprint. + */ + public boolean isFingerprintEnrolled(int userId) { + return mFpEnrolledForUser.getOrDefault(userId, false); + } + private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { mCurrentDialogArgs = args; @@ -1263,6 +1287,16 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, default void onEnrollmentsChanged() {} /** + * Called when UDFPS enrollments have changed. This is called after boot and on changes to + * enrollment. + */ + default void onEnrollmentsChanged( + @NonNull BiometricType biometricType, + int userId, + boolean hasEnrollments + ) {} + + /** * Called when the biometric prompt starts showing. */ default void onBiometricPromptShown() {} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java new file mode 100644 index 000000000000..902bb18d17b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProvider.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics; + +/** Provides the status of the interactive to auth feature. */ +public interface FingerprintInteractiveToAuthProvider { + /** + * + * @param userId the user Id. + * @return true if the InteractiveToAuthFeature is enabled, false if disabled. + */ + boolean isEnabled(int userId); +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt index 4130cf589310..ef7dcb7aac93 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt @@ -190,11 +190,6 @@ abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>( open fun listenForTouchesOutsideView(): Boolean = false /** - * Called on touches outside of the view if listenForTouchesOutsideView returns true - */ - open fun onTouchOutsideView() {} - - /** * Called when a view should announce an accessibility event. */ open fun doAnnounceForAccessibility(str: String) {} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index f3136babada6..cea1779e39b8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -73,6 +73,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -149,6 +150,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator; @NonNull private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; @Nullable private final TouchProcessor mTouchProcessor; + @NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor; // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple // sensors, this, in addition to a lot of the code here, will be updated. @@ -232,12 +234,12 @@ public class UdfpsController implements DozeReceiver, Dumpable { mShadeExpansionStateManager, mKeyguardViewManager, mKeyguardUpdateMonitor, mDialogManager, mDumpManager, mLockscreenShadeTransitionController, mConfigurationController, - mSystemClock, mKeyguardStateController, + mKeyguardStateController, mUnlockedScreenOffAnimationController, mUdfpsDisplayMode, requestId, reason, callback, (view, event, fromUdfpsView) -> onTouch(requestId, event, fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags, - mPrimaryBouncerInteractor))); + mPrimaryBouncerInteractor, mAlternateBouncerInteractor))); } @Override @@ -329,13 +331,13 @@ public class UdfpsController implements DozeReceiver, Dumpable { if (!mOverlayParams.equals(overlayParams)) { mOverlayParams = overlayParams; - final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateBouncer(); + final boolean wasShowingAlternateBouncer = mAlternateBouncerInteractor.isVisibleState(); // When the bounds change it's always necessary to re-create the overlay's window with // new LayoutParams. If the overlay needs to be shown, this will re-create and show the // overlay with the updated LayoutParams. Otherwise, the overlay will remain hidden. redrawOverlay(); - if (wasShowingAltAuth) { + if (wasShowingAlternateBouncer) { mKeyguardViewManager.showBouncer(true); } } @@ -543,9 +545,6 @@ public class UdfpsController implements DozeReceiver, Dumpable { final UdfpsView udfpsView = mOverlay.getOverlayView(); boolean handled = false; switch (event.getActionMasked()) { - case MotionEvent.ACTION_OUTSIDE: - udfpsView.onTouchOutsideView(); - return true; case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_HOVER_ENTER: Trace.beginSection("UdfpsController.onTouch.ACTION_DOWN"); @@ -719,7 +718,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull Optional<Provider<AlternateUdfpsTouchProvider>> alternateTouchProvider, @NonNull @BiometricsBackground Executor biometricsExecutor, @NonNull PrimaryBouncerInteractor primaryBouncerInteractor, - @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor) { + @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor, + @NonNull AlternateBouncerInteractor alternateBouncerInteractor) { mContext = context; mExecution = execution; mVibrator = vibrator; @@ -759,6 +759,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mBiometricExecutor = biometricsExecutor; mPrimaryBouncerInteractor = primaryBouncerInteractor; + mAlternateBouncerInteractor = alternateBouncerInteractor; mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) ? singlePointerTouchProcessor : null; @@ -853,9 +854,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { onFingerUp(mOverlay.getRequestId(), oldView); } final boolean removed = mOverlay.hide(); - if (mKeyguardViewManager.isShowingAlternateBouncer()) { - mKeyguardViewManager.hideAlternateBouncer(true); - } + mKeyguardViewManager.hideAlternateBouncer(true); Log.v(TAG, "hideUdfpsOverlay | removing window: " + removed); } else { Log.v(TAG, "hideUdfpsOverlay | the overlay is already hidden"); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 8db4927ee059..a3c4985fd5cc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -50,6 +50,7 @@ import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager @@ -59,7 +60,6 @@ import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.time.SystemClock private const val TAG = "UdfpsControllerOverlay" @@ -86,7 +86,6 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private val dumpManager: DumpManager, private val transitionController: LockscreenShadeTransitionController, private val configurationController: ConfigurationController, - private val systemClock: SystemClock, private val keyguardStateController: KeyguardStateController, private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController, private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider, @@ -97,7 +96,8 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private val activityLaunchAnimator: ActivityLaunchAnimator, private val featureFlags: FeatureFlags, private val primaryBouncerInteractor: PrimaryBouncerInteractor, - private val isDebuggable: Boolean = Build.IS_DEBUGGABLE + private val alternateBouncerInteractor: AlternateBouncerInteractor, + private val isDebuggable: Boolean = Build.IS_DEBUGGABLE, ) { /** The view, when [isShowing], or null. */ var overlayView: UdfpsView? = null @@ -255,14 +255,14 @@ class UdfpsControllerOverlay @JvmOverloads constructor( dumpManager, transitionController, configurationController, - systemClock, keyguardStateController, unlockedScreenOffAnimationController, dialogManager, controller, activityLaunchAnimator, featureFlags, - primaryBouncerInteractor + primaryBouncerInteractor, + alternateBouncerInteractor, ) } REASON_AUTH_BP -> { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt index 63144fcea761..583ee3ac8e60 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt @@ -31,6 +31,7 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -42,13 +43,13 @@ import com.android.systemui.statusbar.notification.stack.StackStateAnimator import com.android.systemui.statusbar.phone.KeyguardBouncer import com.android.systemui.statusbar.phone.KeyguardBouncer.PrimaryBouncerExpansionCallback import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.AlternateBouncer import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.LegacyAlternateBouncer +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.OccludingAppBiometricUI import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -65,25 +66,27 @@ constructor( dumpManager: DumpManager, private val lockScreenShadeTransitionController: LockscreenShadeTransitionController, private val configurationController: ConfigurationController, - private val systemClock: SystemClock, private val keyguardStateController: KeyguardStateController, private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController, systemUIDialogManager: SystemUIDialogManager, private val udfpsController: UdfpsController, private val activityLaunchAnimator: ActivityLaunchAnimator, featureFlags: FeatureFlags, - private val primaryBouncerInteractor: PrimaryBouncerInteractor + private val primaryBouncerInteractor: PrimaryBouncerInteractor, + private val alternateBouncerInteractor: AlternateBouncerInteractor, ) : UdfpsAnimationViewController<UdfpsKeyguardView>( view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager, - dumpManager + dumpManager, ) { private val useExpandedOverlay: Boolean = featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) private val isModernBouncerEnabled: Boolean = featureFlags.isEnabled(Flags.MODERN_BOUNCER) + private val isModernAlternateBouncerEnabled: Boolean = + featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER) private var showingUdfpsBouncer = false private var udfpsRequested = false private var qsExpansion = 0f @@ -91,7 +94,6 @@ constructor( private var statusBarState = 0 private var transitionToFullShadeProgress = 0f private var lastDozeAmount = 0f - private var lastUdfpsBouncerShowTime: Long = -1 private var panelExpansionFraction = 0f private var launchTransitionFadingAway = false private var isLaunchingActivity = false @@ -244,20 +246,8 @@ constructor( } } - private val mAlternateBouncer: AlternateBouncer = - object : AlternateBouncer { - override fun showAlternateBouncer(): Boolean { - return showUdfpsBouncer(true) - } - - override fun hideAlternateBouncer(): Boolean { - return showUdfpsBouncer(false) - } - - override fun isShowingAlternateBouncer(): Boolean { - return showingUdfpsBouncer - } - + private val occludingAppBiometricUI: OccludingAppBiometricUI = + object : OccludingAppBiometricUI { override fun requestUdfps(request: Boolean, color: Int) { udfpsRequested = request view.requestUdfps(request, color) @@ -275,16 +265,19 @@ constructor( override fun onInit() { super.onInit() - keyguardViewManager.setAlternateBouncer(mAlternateBouncer) + keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI) } init { - if (isModernBouncerEnabled) { + if (isModernBouncerEnabled || isModernAlternateBouncerEnabled) { view.repeatWhenAttached { // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion // can make the view not visible; and we still want to listen for events // that may make the view visible again. - repeatOnLifecycle(Lifecycle.State.CREATED) { listenForBouncerExpansion(this) } + repeatOnLifecycle(Lifecycle.State.CREATED) { + if (isModernBouncerEnabled) listenForBouncerExpansion(this) + if (isModernAlternateBouncerEnabled) listenForAlternateBouncerVisibility(this) + } } } } @@ -300,8 +293,18 @@ constructor( } } + @VisibleForTesting + internal suspend fun listenForAlternateBouncerVisibility(scope: CoroutineScope): Job { + return scope.launch { + alternateBouncerInteractor.isVisible.collect { isVisible: Boolean -> + showUdfpsBouncer(isVisible) + } + } + } + public override fun onViewAttached() { super.onViewAttached() + alternateBouncerInteractor.setAlternateBouncerUIAvailable(true) val dozeAmount = statusBarStateController.dozeAmount lastDozeAmount = dozeAmount stateListener.onDozeAmountChanged(dozeAmount, dozeAmount) @@ -326,7 +329,8 @@ constructor( view.updatePadding() updateAlpha() updatePauseAuth() - keyguardViewManager.setAlternateBouncer(mAlternateBouncer) + keyguardViewManager.setLegacyAlternateBouncer(legacyAlternateBouncer) + keyguardViewManager.setOccludingAppBiometricUI(occludingAppBiometricUI) lockScreenShadeTransitionController.udfpsKeyguardViewController = this activityLaunchAnimator.addListener(activityLaunchAnimatorListener) view.mUseExpandedOverlay = useExpandedOverlay @@ -334,10 +338,12 @@ constructor( override fun onViewDetached() { super.onViewDetached() + alternateBouncerInteractor.setAlternateBouncerUIAvailable(false) faceDetectRunning = false keyguardStateController.removeCallback(keyguardStateControllerCallback) statusBarStateController.removeCallback(stateListener) - keyguardViewManager.removeAlternateAuthInterceptor(mAlternateBouncer) + keyguardViewManager.removeLegacyAlternateBouncer(legacyAlternateBouncer) + keyguardViewManager.removeOccludingAppBiometricUI(occludingAppBiometricUI) keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false) configurationController.removeCallback(configurationListener) shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener) @@ -356,7 +362,16 @@ constructor( override fun dump(pw: PrintWriter, args: Array<String>) { super.dump(pw, args) pw.println("isModernBouncerEnabled=$isModernBouncerEnabled") + pw.println("isModernAlternateBouncerEnabled=$isModernAlternateBouncerEnabled") pw.println("showingUdfpsAltBouncer=$showingUdfpsBouncer") + pw.println( + "altBouncerInteractor#isAlternateBouncerVisible=" + + "${alternateBouncerInteractor.isVisibleState()}" + ) + pw.println( + "altBouncerInteractor#canShowAlternateBouncerForFingerprint=" + + "${alternateBouncerInteractor.canShowAlternateBouncerForFingerprint()}" + ) pw.println("faceDetectRunning=$faceDetectRunning") pw.println("statusBarState=" + StatusBarState.toString(statusBarState)) pw.println("transitionToFullShadeProgress=$transitionToFullShadeProgress") @@ -385,9 +400,6 @@ constructor( val udfpsAffordanceWasNotShowing = shouldPauseAuth() showingUdfpsBouncer = show if (showingUdfpsBouncer) { - lastUdfpsBouncerShowTime = systemClock.uptimeMillis() - } - if (showingUdfpsBouncer) { if (udfpsAffordanceWasNotShowing) { view.animateInUdfpsBouncer(null) } @@ -452,7 +464,7 @@ constructor( return if (isModernBouncerEnabled) { inputBouncerExpansion == 1f } else { - keyguardViewManager.isBouncerShowing && !keyguardViewManager.isShowingAlternateBouncer + keyguardViewManager.isBouncerShowing && !alternateBouncerInteractor.isVisibleState() } } @@ -460,30 +472,6 @@ constructor( return true } - override fun onTouchOutsideView() { - maybeShowInputBouncer() - } - - /** - * If we were previously showing the udfps bouncer, hide it and instead show the regular - * (pin/pattern/password) bouncer. - * - * Does nothing if we weren't previously showing the UDFPS bouncer. - */ - private fun maybeShowInputBouncer() { - if (showingUdfpsBouncer && hasUdfpsBouncerShownWithMinTime()) { - keyguardViewManager.showPrimaryBouncer(true) - } - } - - /** - * Whether the udfps bouncer has shown for at least 200ms before allowing touches outside of the - * udfps icon area to dismiss the udfps bouncer and show the pin/pattern/password bouncer. - */ - private fun hasUdfpsBouncerShownWithMinTime(): Boolean { - return systemClock.uptimeMillis() - lastUdfpsBouncerShowTime > 200 - } - /** * Set the progress we're currently transitioning to the full shade. 0.0f means we're not * transitioning yet, while 1.0f means we've fully dragged down. For example, start swiping down @@ -545,7 +533,7 @@ constructor( if (isModernBouncerEnabled) { return } - val altBouncerShowing = keyguardViewManager.isShowingAlternateBouncer + val altBouncerShowing = alternateBouncerInteractor.isVisibleState() if (altBouncerShowing || !keyguardViewManager.primaryBouncerIsOrWillBeShowing()) { inputBouncerHiddenAmount = 1f } else if (keyguardViewManager.isBouncerShowing) { @@ -554,6 +542,21 @@ constructor( } } + private val legacyAlternateBouncer: LegacyAlternateBouncer = + object : LegacyAlternateBouncer { + override fun showAlternateBouncer(): Boolean { + return showUdfpsBouncer(true) + } + + override fun hideAlternateBouncer(): Boolean { + return showUdfpsBouncer(false) + } + + override fun isShowingAlternateBouncer(): Boolean { + return showingUdfpsBouncer + } + } + companion object { const val TAG = "UdfpsKeyguardViewController" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt index 4a8877edfa53..e61c614f0292 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt @@ -111,10 +111,6 @@ class UdfpsView( } } - fun onTouchOutsideView() { - animationViewController?.onTouchOutsideView() - } - override fun onAttachedToWindow() { super.onAttachedToWindow() Log.v(TAG, "onAttachedToWindow") diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt index 857224290752..682d38a8f1a8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetector.kt @@ -18,6 +18,7 @@ package com.android.systemui.biometrics.udfps import android.graphics.Point import android.graphics.Rect +import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import kotlin.math.cos import kotlin.math.pow @@ -50,7 +51,8 @@ class EllipseOverlapDetector(private val neededPoints: Int = 2) : OverlapDetecto return result <= 1 } - private fun calculateSensorPoints(sensorBounds: Rect): List<Point> { + @VisibleForTesting + fun calculateSensorPoints(sensorBounds: Rect): List<Point> { val sensorX = sensorBounds.centerX() val sensorY = sensorBounds.centerY() val cornerOffset: Int = sensorBounds.width() / 4 diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt index 338bf66d197e..693f64a1f93d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt @@ -27,6 +27,8 @@ import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject +private val SUPPORTED_ROTATIONS = setOf(Surface.ROTATION_90, Surface.ROTATION_270) + /** * TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations. */ @@ -129,19 +131,27 @@ private fun MotionEvent.normalize( val nativeY = naturalTouch.y / overlayParams.scaleFactor val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor + var nativeOrientation: Float = getOrientation(pointerIndex) + if (SUPPORTED_ROTATIONS.contains(overlayParams.rotation)) { + nativeOrientation = toRadVerticalFromRotated(nativeOrientation.toDouble()).toFloat() + } return NormalizedTouchData( pointerId = getPointerId(pointerIndex), x = nativeX, y = nativeY, minor = nativeMinor, major = nativeMajor, - // TODO(b/259311354): touch orientation should be reported relative to Surface.ROTATION_O. - orientation = getOrientation(pointerIndex), + orientation = nativeOrientation, time = eventTime, gestureStart = downTime, ) } +private fun toRadVerticalFromRotated(rad: Double): Double { + val piBound = ((rad % Math.PI) + Math.PI / 2) % Math.PI + return if (piBound < Math.PI / 2.0) piBound else piBound - Math.PI +} + /** * Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device * is in the [Surface.ROTATION_0] orientation. @@ -152,7 +162,7 @@ private fun MotionEvent.rotateToNaturalOrientation( ): PointF { val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex)) val rot = overlayParams.rotation - if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { + if (SUPPORTED_ROTATIONS.contains(rot)) { RotationUtils.rotatePointF( touchPoint, RotationUtils.deltaRotation(rot, Surface.ROTATION_0), diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index e8e1f2e95f5d..e9ac840cf4f4 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -176,7 +176,8 @@ public class BrightLineFalsingManager implements FalsingManager { private @Classifier.InteractionType int mPriorInteractionType = Classifier.GENERIC; @Inject - public BrightLineFalsingManager(FalsingDataProvider falsingDataProvider, + public BrightLineFalsingManager( + FalsingDataProvider falsingDataProvider, MetricsLogger metricsLogger, @Named(BRIGHT_LINE_GESTURE_CLASSIFERS) Set<FalsingClassifier> classifiers, SingleTapClassifier singleTapClassifier, LongTapClassifier longTapClassifier, @@ -399,7 +400,9 @@ public class BrightLineFalsingManager implements FalsingManager { || mDataProvider.isJustUnlockedWithFace() || mDataProvider.isDocked() || mAccessibilityManager.isTouchExplorationEnabled() - || mDataProvider.isA11yAction(); + || mDataProvider.isA11yAction() + || (mFeatureFlags.isEnabled(Flags.FALSING_OFF_FOR_UNFOLDED) + && !mDataProvider.isFolded()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java index 09ebeeac163f..5f347c158818 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java @@ -16,6 +16,7 @@ package com.android.systemui.classifier; +import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; @@ -42,6 +43,7 @@ public class FalsingDataProvider { private final int mWidthPixels; private final int mHeightPixels; private BatteryController mBatteryController; + private final FoldStateListener mFoldStateListener; private final DockManager mDockManager; private final float mXdpi; private final float mYdpi; @@ -65,12 +67,14 @@ public class FalsingDataProvider { public FalsingDataProvider( DisplayMetrics displayMetrics, BatteryController batteryController, + FoldStateListener foldStateListener, DockManager dockManager) { mXdpi = displayMetrics.xdpi; mYdpi = displayMetrics.ydpi; mWidthPixels = displayMetrics.widthPixels; mHeightPixels = displayMetrics.heightPixels; mBatteryController = batteryController; + mFoldStateListener = foldStateListener; mDockManager = dockManager; FalsingClassifier.logInfo("xdpi, ydpi: " + getXdpi() + ", " + getYdpi()); @@ -376,6 +380,10 @@ public class FalsingDataProvider { return mBatteryController.isWirelessCharging() || mDockManager.isDocked(); } + public boolean isFolded() { + return Boolean.TRUE.equals(mFoldStateListener.getFolded()); + } + /** Implement to be alerted abotu the beginning and ending of falsing tracking. */ public interface SessionListener { /** Called when the lock screen is shown and falsing-tracking begins. */ diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt index eed55315e836..9b2a224f17e0 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingController.kt @@ -51,13 +51,22 @@ interface ControlsBindingController : UserAwareController { fun bindAndLoadSuggested(component: ComponentName, callback: LoadCallback) /** - * Request to bind to the given service. + * Request to bind to the given service. This should only be used for services using the full + * [ControlsProviderService] API, where SystemUI renders the devices' UI. * * @param component The [ComponentName] of the service to bind */ fun bindService(component: ComponentName) /** + * Bind to a service that provides a Device Controls panel (embedded activity). This will allow + * the app to remain "warm", and reduce latency. + * + * @param component The [ComponentName] of the [ControlsProviderService] to bind. + */ + fun bindServiceForPanel(component: ComponentName) + + /** * Send a subscribe message to retrieve status of a set of controls. * * @param structureInfo structure containing the controls to update diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt index 2f0fd99337e5..3d6d3356fb55 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt @@ -170,6 +170,10 @@ open class ControlsBindingControllerImpl @Inject constructor( retrieveLifecycleManager(component).bindService() } + override fun bindServiceForPanel(component: ComponentName) { + retrieveLifecycleManager(component).bindServiceForPanel() + } + override fun changeUser(newUser: UserHandle) { if (newUser == currentUser) return diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index 2f49c3fe863e..f29f6d0dd0cb 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -189,6 +189,14 @@ interface ControlsController : UserAwareController { fun getPreferredSelection(): SelectedItem /** + * Bind to a service that provides a Device Controls panel (embedded activity). This will allow + * the app to remain "warm", and reduce latency. + * + * @param component The [ComponentName] of the [ControlsProviderService] to bind. + */ + fun bindComponentForPanel(componentName: ComponentName) + + /** * Interface for structure to pass data to [ControlsFavoritingActivity]. */ interface LoadData { diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 80c5f661f9a3..111fcbbe30be 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -477,6 +477,10 @@ class ControlsControllerImpl @Inject constructor ( bindingController.unsubscribe() } + override fun bindComponentForPanel(componentName: ComponentName) { + bindingController.bindServiceForPanel(componentName) + } + override fun addFavorite( componentName: ComponentName, structureName: CharSequence, diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt index 5b38e5b28be9..72c3a943c30b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt @@ -78,6 +78,10 @@ class ControlsProviderLifecycleManager( private const val DEBUG = true private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or Context.BIND_NOT_PERCEPTIBLE + // Use BIND_NOT_PERCEPTIBLE so it will be at lower priority from SystemUI. + // However, don't use WAIVE_PRIORITY, as by itself, it will kill the app + // once the Task is finished in the device controls panel. + private val BIND_FLAGS_PANEL = Context.BIND_AUTO_CREATE or Context.BIND_NOT_PERCEPTIBLE } private val intent = Intent().apply { @@ -87,18 +91,19 @@ class ControlsProviderLifecycleManager( }) } - private fun bindService(bind: Boolean) { + private fun bindService(bind: Boolean, forPanel: Boolean = false) { executor.execute { requiresBound = bind if (bind) { - if (bindTryCount != MAX_BIND_RETRIES) { + if (bindTryCount != MAX_BIND_RETRIES && wrapper == null) { if (DEBUG) { Log.d(TAG, "Binding service $intent") } bindTryCount++ try { + val flags = if (forPanel) BIND_FLAGS_PANEL else BIND_FLAGS val bound = context - .bindServiceAsUser(intent, serviceConnection, BIND_FLAGS, user) + .bindServiceAsUser(intent, serviceConnection, flags, user) if (!bound) { context.unbindService(serviceConnection) } @@ -279,6 +284,10 @@ class ControlsProviderLifecycleManager( bindService(true) } + fun bindServiceForPanel() { + bindService(bind = true, forPanel = true) + } + /** * Request unbind from the service. */ diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 1e3e5cd1c31c..6289788f650a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -232,6 +232,8 @@ class ControlsUiControllerImpl @Inject constructor ( ControlKey(selected.structure.componentName, it.ci.controlId) } controlsController.get().subscribeToFavorites(selected.structure) + } else { + controlsController.get().bindComponentForPanel(selected.componentName) } listingCallback = createCallback(::showControlsView) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index b8e66735c740..6d13740afe8b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -31,6 +31,7 @@ import com.android.systemui.BootCompleteCacheImpl; import com.android.systemui.appops.dagger.AppOpsModule; import com.android.systemui.assist.AssistModule; import com.android.systemui.biometrics.AlternateUdfpsTouchProvider; +import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; import com.android.systemui.biometrics.dagger.BiometricsModule; import com.android.systemui.biometrics.dagger.UdfpsModule; @@ -221,6 +222,9 @@ public abstract class SystemUIModule { @BindsOptionalOf abstract AlternateUdfpsTouchProvider optionalUdfpsTouchProvider(); + @BindsOptionalOf + abstract FingerprintInteractiveToAuthProvider optionalFingerprintInteractiveToAuthProvider(); + @SysUISingleton @Binds abstract SystemClock bindSystemClock(SystemClockImpl systemClock); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java index f244cb009ba4..96bce4cd3cd9 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java @@ -19,6 +19,7 @@ package com.android.systemui.dreams; import android.annotation.IntDef; import android.annotation.Nullable; import android.content.Context; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -26,6 +27,9 @@ import android.view.ViewGroup; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.systemui.R; +import com.android.systemui.shared.shadow.DoubleShadowIconDrawable; +import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo; +import com.android.systemui.statusbar.AlphaOptimizedImageView; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -60,8 +64,15 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { public static final int STATUS_ICON_PRIORITY_MODE_ON = 6; private final Map<Integer, View> mStatusIcons = new HashMap<>(); + private Context mContext; private ViewGroup mSystemStatusViewGroup; private ViewGroup mExtraSystemStatusViewGroup; + private ShadowInfo mKeyShadowInfo; + private ShadowInfo mAmbientShadowInfo; + private int mDrawableSize; + private int mDrawableInsetSize; + private static final float KEY_SHADOW_ALPHA = 0.35f; + private static final float AMBIENT_SHADOW_ALPHA = 0.4f; public DreamOverlayStatusBarView(Context context) { this(context, null); @@ -73,6 +84,7 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { public DreamOverlayStatusBarView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); + mContext = context; } public DreamOverlayStatusBarView( @@ -80,14 +92,36 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { super(context, attrs, defStyleAttr, defStyleRes); } + @Override protected void onFinishInflate() { super.onFinishInflate(); + mKeyShadowInfo = createShadowInfo( + R.dimen.dream_overlay_status_bar_key_text_shadow_radius, + R.dimen.dream_overlay_status_bar_key_text_shadow_dx, + R.dimen.dream_overlay_status_bar_key_text_shadow_dy, + KEY_SHADOW_ALPHA + ); + + mAmbientShadowInfo = createShadowInfo( + R.dimen.dream_overlay_status_bar_ambient_text_shadow_radius, + R.dimen.dream_overlay_status_bar_ambient_text_shadow_dx, + R.dimen.dream_overlay_status_bar_ambient_text_shadow_dy, + AMBIENT_SHADOW_ALPHA + ); + + mDrawableSize = mContext + .getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_status_bar_icon_size); + mDrawableInsetSize = mContext + .getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_icon_inset_dimen); + mStatusIcons.put(STATUS_ICON_WIFI_UNAVAILABLE, - fetchStatusIconForResId(R.id.dream_overlay_wifi_status)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_wifi_status))); mStatusIcons.put(STATUS_ICON_ALARM_SET, - fetchStatusIconForResId(R.id.dream_overlay_alarm_set)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_alarm_set))); mStatusIcons.put(STATUS_ICON_CAMERA_DISABLED, fetchStatusIconForResId(R.id.dream_overlay_camera_off)); mStatusIcons.put(STATUS_ICON_MIC_DISABLED, @@ -97,7 +131,7 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { mStatusIcons.put(STATUS_ICON_NOTIFICATIONS, fetchStatusIconForResId(R.id.dream_overlay_notification_indicator)); mStatusIcons.put(STATUS_ICON_PRIORITY_MODE_ON, - fetchStatusIconForResId(R.id.dream_overlay_priority_mode)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_priority_mode))); mSystemStatusViewGroup = findViewById(R.id.dream_overlay_system_status); mExtraSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items); @@ -137,4 +171,34 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { } return false; } + + private View addDoubleShadow(View icon) { + if (icon instanceof AlphaOptimizedImageView) { + AlphaOptimizedImageView i = (AlphaOptimizedImageView) icon; + Drawable drawableIcon = i.getDrawable(); + i.setImageDrawable(new DoubleShadowIconDrawable( + mKeyShadowInfo, + mAmbientShadowInfo, + drawableIcon, + mDrawableSize, + mDrawableInsetSize + )); + } + return icon; + } + + private ShadowInfo createShadowInfo(int blurId, int offsetXId, int offsetYId, float alpha) { + return new ShadowInfo( + fetchDimensionForResId(blurId), + fetchDimensionForResId(offsetXId), + fetchDimensionForResId(offsetYId), + alpha + ); + } + + private Float fetchDimensionForResId(int resId) { + return mContext + .getResources() + .getDimension(resId); + } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index ae8caadd7561..6958f3b1bfe1 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -114,8 +114,6 @@ object Flags { // ** Flag retired ** // public static final BooleanFlag KEYGUARD_LAYOUT = // new BooleanFlag(200, true); - // TODO(b/254512713): Tracking Bug - @JvmField val LOCKSCREEN_ANIMATIONS = releasedFlag(201, "lockscreen_animations") // TODO(b/254512750): Tracking Bug val NEW_UNLOCK_SWIPE_ANIMATION = releasedFlag(202, "new_unlock_swipe_animation") @@ -180,6 +178,13 @@ object Flags { @JvmField val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = false) + /** + * Whether to use the new alternate bouncer architecture, a refactor of and eventual replacement + * of the Alternate/Authentication Bouncer. No visual UI changes. + */ + // TODO(b/260619425): Tracking Bug + @JvmField val MODERN_ALTERNATE_BOUNCER = unreleasedFlag(219, "modern_alternate_bouncer") + /** Flag to control the migration of face auth to modern architecture. */ // TODO(b/262838215): Tracking bug @JvmField val FACE_AUTH_REFACTOR = unreleasedFlag(220, "face_auth_refactor") @@ -195,13 +200,16 @@ object Flags { /** A different path for unocclusion transitions back to keyguard */ // TODO(b/262859270): Tracking Bug @JvmField - val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false) + val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = true) // flag for controlling auto pin confirmation and material u shapes in bouncer @JvmField val AUTO_PIN_CONFIRMATION = unreleasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation") + // TODO(b/262859270): Tracking Bug + @JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded") + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite") @@ -255,10 +263,11 @@ object Flags { // TODO(b/256614751): Tracking Bug val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND = - unreleasedFlag(608, "new_status_bar_mobile_icons_backend") + unreleasedFlag(608, "new_status_bar_mobile_icons_backend", teamfood = true) // TODO(b/256613548): Tracking Bug - val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend") + val NEW_STATUS_BAR_WIFI_ICON_BACKEND = + unreleasedFlag(609, "new_status_bar_wifi_icon_backend", teamfood = true) // TODO(b/256623670): Tracking Bug @JvmField @@ -297,7 +306,7 @@ object Flags { // 900 - media // TODO(b/254512697): Tracking Bug - val MEDIA_TAP_TO_TRANSFER = unreleasedFlag(900, "media_tap_to_transfer", teamfood = true) + val MEDIA_TAP_TO_TRANSFER = releasedFlag(900, "media_tap_to_transfer") // TODO(b/254512502): Tracking Bug val MEDIA_SESSION_ACTIONS = unreleasedFlag(901, "media_session_actions") @@ -327,13 +336,17 @@ object Flags { val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE = unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true) + // TODO(b/263512203): Tracking Bug + val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true) + // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") // TODO(b/254512758): Tracking Bug @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple") - val SHOW_LOWLIGHT_ON_DIRECT_BOOT = unreleasedFlag(1003, "show_lowlight_on_direct_boot") + // TODO(b/265045965): Tracking Bug + val SHOW_LOWLIGHT_ON_DIRECT_BOOT = releasedFlag(1003, "show_lowlight_on_direct_boot") // 1100 - windowing @Keep diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 8200f259a992..fe84ac5a32bc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -1926,13 +1926,23 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, return; } - // if the keyguard is already showing, don't bother. check flags in both files - // to account for the hiding animation which results in a delay and discrepancy - // between flags + // If the keyguard is already showing, see if we don't need to bother re-showing it. Check + // flags in both files to account for the hiding animation which results in a delay and + // discrepancy between flags. if (mShowing && mKeyguardStateController.isShowing()) { - if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); - resetStateLocked(); - return; + if (mPM.isInteractive()) { + // It's already showing, and we're not trying to show it while the screen is off. + // We can simply reset all of the views. + if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing"); + resetStateLocked(); + return; + } else { + // We are trying to show the keyguard while the screen is off - this results from + // race conditions involving locking while unlocking. Don't short-circuit here and + // ensure the keyguard is fully re-shown. + Log.e(TAG, + "doKeyguard: already showing, but re-showing since we're not interactive"); + } } // In split system user mode, we never unlock system user. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java index 017b65acd1d2..ffd8a0244a86 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java @@ -33,6 +33,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.util.time.SystemClock; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -63,6 +64,7 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe private final Context mContext; private final DisplayMetrics mDisplayMetrics; + private final SystemClock mSystemClock; @Nullable private final IWallpaperManager mWallpaperManagerService; @@ -71,6 +73,9 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe private @PowerManager.WakeReason int mLastWakeReason = PowerManager.WAKE_REASON_UNKNOWN; + public static final long UNKNOWN_LAST_WAKE_TIME = -1; + private long mLastWakeTime = UNKNOWN_LAST_WAKE_TIME; + @Nullable private Point mLastWakeOriginLocation = null; @@ -84,10 +89,12 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe public WakefulnessLifecycle( Context context, @Nullable IWallpaperManager wallpaperManagerService, + SystemClock systemClock, DumpManager dumpManager) { mContext = context; mDisplayMetrics = context.getResources().getDisplayMetrics(); mWallpaperManagerService = wallpaperManagerService; + mSystemClock = systemClock; dumpManager.registerDumpable(getClass().getSimpleName(), this); } @@ -104,6 +111,14 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe } /** + * Returns the most recent time (in device uptimeMillis) the display woke up. + * Returns {@link UNKNOWN_LAST_WAKE_TIME} if there hasn't been a wakeup yet. + */ + public long getLastWakeTime() { + return mLastWakeTime; + } + + /** * Returns the most recent reason the device went to sleep up. This is one of * PowerManager.GO_TO_SLEEP_REASON_*. */ @@ -117,6 +132,7 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe } setWakefulness(WAKEFULNESS_WAKING); mLastWakeReason = pmWakeReason; + mLastWakeTime = mSystemClock.uptimeMillis(); updateLastWakeOriginLocation(); if (mWallpaperManagerService != null) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt new file mode 100644 index 000000000000..25d8f4021f87 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricRepository.kt @@ -0,0 +1,184 @@ +/* + * 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.keyguard.data.repository + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED +import android.content.Context +import android.content.IntentFilter +import android.os.Looper +import android.os.UserHandle +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.biometrics.AuthController +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest + +/** + * Acts as source of truth for biometric features. + * + * Abstracts-away data sources and their schemas so the rest of the app doesn't need to worry about + * upstream changes. + */ +interface BiometricRepository { + /** Whether any fingerprints are enrolled for the current user. */ + val isFingerprintEnrolled: StateFlow<Boolean> + + /** + * Whether the current user is allowed to use a strong biometric for device entry based on + * Android Security policies. If false, the user may be able to use primary authentication for + * device entry. + */ + val isStrongBiometricAllowed: StateFlow<Boolean> + + /** Whether fingerprint feature is enabled for the current user by the DevicePolicy */ + val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> +} + +@SysUISingleton +class BiometricRepositoryImpl +@Inject +constructor( + context: Context, + lockPatternUtils: LockPatternUtils, + broadcastDispatcher: BroadcastDispatcher, + authController: AuthController, + userRepository: UserRepository, + devicePolicyManager: DevicePolicyManager, + @Application scope: CoroutineScope, + @Background backgroundDispatcher: CoroutineDispatcher, + @Main looper: Looper, +) : BiometricRepository { + + /** UserId of the current selected user. */ + private val selectedUserId: Flow<Int> = + userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged() + + override val isFingerprintEnrolled: StateFlow<Boolean> = + selectedUserId + .flatMapLatest { userId -> + conflatedCallbackFlow { + val callback = + object : AuthController.Callback { + override fun onEnrollmentsChanged( + sensorBiometricType: BiometricType, + userId: Int, + hasEnrollments: Boolean + ) { + if (sensorBiometricType.isFingerprint) { + trySendWithFailureLogging( + hasEnrollments, + TAG, + "update fpEnrollment" + ) + } + } + } + authController.addCallback(callback) + awaitClose { authController.removeCallback(callback) } + } + } + .stateIn( + scope, + started = SharingStarted.Eagerly, + initialValue = + authController.isFingerprintEnrolled(userRepository.getSelectedUserInfo().id) + ) + + override val isStrongBiometricAllowed: StateFlow<Boolean> = + selectedUserId + .flatMapLatest { currUserId -> + conflatedCallbackFlow { + val callback = + object : LockPatternUtils.StrongAuthTracker(context, looper) { + override fun onStrongAuthRequiredChanged(userId: Int) { + if (currUserId != userId) { + return + } + + trySendWithFailureLogging( + isBiometricAllowedForUser(true, currUserId), + TAG + ) + } + + override fun onIsNonStrongBiometricAllowedChanged(userId: Int) { + // no-op + } + } + lockPatternUtils.registerStrongAuthTracker(callback) + awaitClose { lockPatternUtils.unregisterStrongAuthTracker(callback) } + } + } + .stateIn( + scope, + started = SharingStarted.Eagerly, + initialValue = + lockPatternUtils.isBiometricAllowedForUser( + userRepository.getSelectedUserInfo().id + ) + ) + + override val isFingerprintEnabledByDevicePolicy: StateFlow<Boolean> = + selectedUserId + .flatMapLatest { userId -> + broadcastDispatcher + .broadcastFlow( + filter = IntentFilter(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), + user = UserHandle.ALL + ) + .transformLatest { + emit( + (devicePolicyManager.getKeyguardDisabledFeatures(null, userId) and + DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) == 0 + ) + } + .flowOn(backgroundDispatcher) + .distinctUntilChanged() + } + .stateIn( + scope, + started = SharingStarted.Eagerly, + initialValue = + devicePolicyManager.getKeyguardDisabledFeatures( + null, + userRepository.getSelectedUserInfo().id + ) and DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT == 0 + ) + + companion object { + private const val TAG = "BiometricsRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt new file mode 100644 index 000000000000..93c97813d8e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricType.kt @@ -0,0 +1,33 @@ +/* + * 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.keyguard.data.repository + +enum class BiometricType(val isFingerprint: Boolean) { + // An unsupported biometric type + UNKNOWN(false), + + // Fingerprint sensor that is located on the back (opposite side of the display) of the device + REAR_FINGERPRINT(true), + + // Fingerprint sensor that is located under the display + UNDER_DISPLAY_FINGERPRINT(true), + + // Fingerprint sensor that is located on the side of the device, typically on the power button + SIDE_FINGERPRINT(true), + FACE(false), +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt index 90f3c7d88c8f..2e34e9a93dff 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt @@ -26,9 +26,11 @@ import com.android.systemui.log.dagger.BouncerLog import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.phone.KeyguardBouncer +import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn @@ -44,6 +46,7 @@ class KeyguardBouncerRepository @Inject constructor( private val viewMediatorCallback: ViewMediatorCallback, + private val clock: SystemClock, @Application private val applicationScope: CoroutineScope, @BouncerLog private val buffer: TableLogBuffer, ) { @@ -94,6 +97,14 @@ constructor( setUpLogging() } + /** Values associated with the AlternateBouncer */ + private val _isAlternateBouncerVisible = MutableStateFlow(false) + val isAlternateBouncerVisible = _isAlternateBouncerVisible.asStateFlow() + var lastAlternateBouncerVisibleTime: Long = NOT_VISIBLE + private val _isAlternateBouncerUIAvailable = MutableStateFlow<Boolean>(false) + val isAlternateBouncerUIAvailable: StateFlow<Boolean> = + _isAlternateBouncerUIAvailable.asStateFlow() + fun setPrimaryScrimmed(isScrimmed: Boolean) { _primaryBouncerScrimmed.value = isScrimmed } @@ -102,6 +113,19 @@ constructor( _primaryBouncerVisible.value = isVisible } + fun setAlternateVisible(isVisible: Boolean) { + if (isVisible && !_isAlternateBouncerVisible.value) { + lastAlternateBouncerVisibleTime = clock.uptimeMillis() + } else if (!isVisible) { + lastAlternateBouncerVisibleTime = NOT_VISIBLE + } + _isAlternateBouncerVisible.value = isVisible + } + + fun setAlternateBouncerUIAvailable(isAvailable: Boolean) { + _isAlternateBouncerUIAvailable.value = isAvailable + } + fun setPrimaryShow(keyguardBouncerModel: KeyguardBouncerModel?) { _primaryBouncerShow.value = keyguardBouncerModel } @@ -202,4 +226,8 @@ constructor( .logDiffsForTable(buffer, "", "ResourceUpdateRequests", false) .launchIn(applicationScope) } + + companion object { + private const val NOT_VISIBLE = -1L + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index a4fd087a24b1..d99af90ab6dc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -40,6 +40,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode +import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose @@ -88,6 +89,9 @@ interface KeyguardRepository { /** Observable for whether the bouncer is showing. */ val isBouncerShowing: Flow<Boolean> + /** Is the always-on display available to be used? */ + val isAodAvailable: Flow<Boolean> + /** * Observable for whether we are in doze state. * @@ -182,6 +186,7 @@ constructor( private val keyguardStateController: KeyguardStateController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dozeTransitionListener: DozeTransitionListener, + private val dozeParameters: DozeParameters, private val authController: AuthController, private val dreamOverlayCallbackController: DreamOverlayCallbackController, ) : KeyguardRepository { @@ -220,6 +225,31 @@ constructor( } .distinctUntilChanged() + override val isAodAvailable: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : DozeParameters.Callback { + override fun onAlwaysOnChange() { + trySendWithFailureLogging( + dozeParameters.getAlwaysOn(), + TAG, + "updated isAodAvailable" + ) + } + } + + dozeParameters.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + dozeParameters.getAlwaysOn(), + TAG, + "initial isAodAvailable" + ) + + awaitClose { dozeParameters.removeCallback(callback) } + } + .distinctUntilChanged() + override val isKeyguardOccluded: Flow<Boolean> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt index 26f853f3ad1c..4639597a9b8c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt @@ -30,4 +30,6 @@ interface KeyguardRepositoryModule { @Binds fun lightRevealScrimRepository(impl: LightRevealScrimRepositoryImpl): LightRevealScrimRepository + + @Binds fun biometricRepository(impl: BiometricRepositoryImpl): BiometricRepository } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index d14b66a68f11..0c4bca616e12 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -209,7 +209,7 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio return } - if (state == TransitionState.FINISHED) { + if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) { updateTransitionId = null } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt new file mode 100644 index 000000000000..28c0b288147b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractor.kt @@ -0,0 +1,126 @@ +/* + * 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.keyguard.domain.interactor + +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.BiometricRepository +import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.LegacyAlternateBouncer +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Encapsulates business logic for interacting with the lock-screen alternate bouncer. */ +@SysUISingleton +class AlternateBouncerInteractor +@Inject +constructor( + private val bouncerRepository: KeyguardBouncerRepository, + private val biometricRepository: BiometricRepository, + private val systemClock: SystemClock, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + featureFlags: FeatureFlags, +) { + val isModernAlternateBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER) + var legacyAlternateBouncer: LegacyAlternateBouncer? = null + var legacyAlternateBouncerVisibleTime: Long = NOT_VISIBLE + + val isVisible: Flow<Boolean> = bouncerRepository.isAlternateBouncerVisible + + /** + * Sets the correct bouncer states to show the alternate bouncer if it can show. + * @return whether alternateBouncer is visible + */ + fun show(): Boolean { + return when { + isModernAlternateBouncerEnabled -> { + bouncerRepository.setAlternateVisible(canShowAlternateBouncerForFingerprint()) + isVisibleState() + } + canShowAlternateBouncerForFingerprint() -> { + if (legacyAlternateBouncer?.showAlternateBouncer() == true) { + legacyAlternateBouncerVisibleTime = systemClock.uptimeMillis() + true + } else { + false + } + } + else -> false + } + } + + /** + * Sets the correct bouncer states to hide the bouncer. Should only be called through + * StatusBarKeyguardViewManager until ScrimController is refactored to use + * alternateBouncerInteractor. + * @return true if the alternate bouncer was newly hidden, else false. + */ + fun hide(): Boolean { + return if (isModernAlternateBouncerEnabled) { + val wasAlternateBouncerVisible = isVisibleState() + bouncerRepository.setAlternateVisible(false) + wasAlternateBouncerVisible && !isVisibleState() + } else { + legacyAlternateBouncer?.hideAlternateBouncer() ?: false + } + } + + fun isVisibleState(): Boolean { + return if (isModernAlternateBouncerEnabled) { + bouncerRepository.isAlternateBouncerVisible.value + } else { + legacyAlternateBouncer?.isShowingAlternateBouncer ?: false + } + } + + fun setAlternateBouncerUIAvailable(isAvailable: Boolean) { + bouncerRepository.setAlternateBouncerUIAvailable(isAvailable) + } + + fun canShowAlternateBouncerForFingerprint(): Boolean { + return if (isModernAlternateBouncerEnabled) { + bouncerRepository.isAlternateBouncerUIAvailable.value && + biometricRepository.isFingerprintEnrolled.value && + biometricRepository.isStrongBiometricAllowed.value && + biometricRepository.isFingerprintEnabledByDevicePolicy.value + } else { + legacyAlternateBouncer != null && + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(true) + } + } + + /** + * Whether the alt bouncer has shown for a minimum time before allowing touches to dismiss the + * alternate bouncer and show the primary bouncer. + */ + fun hasAlternateBouncerShownWithMinTime(): Boolean { + return if (isModernAlternateBouncerEnabled) { + (systemClock.uptimeMillis() - bouncerRepository.lastAlternateBouncerVisibleTime) > + MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS + } else { + systemClock.uptimeMillis() - legacyAlternateBouncerVisibleTime > 200 + } + } + + companion object { + private const val MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS = 200L + private const val NOT_VISIBLE = -1L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index fd2d271e40f9..ce61f2fec92f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -21,9 +21,9 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration @@ -48,12 +48,11 @@ constructor( private fun listenForDozingToLockscreen() { scope.launch { - keyguardInteractor.dozeTransitionModel + keyguardInteractor.wakefulnessModel .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (dozeTransitionModel, lastStartedTransition) = pair + .collect { (wakefulnessModel, lastStartedTransition) -> if ( - isDozeOff(dozeTransitionModel.to) && + isWakingOrStartingToWake(wakefulnessModel) && lastStartedTransition.to == KeyguardState.DOZING ) { keyguardTransitionRepository.startTransition( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 3b09ae7ba8ea..7134ec0d64f0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -21,7 +21,7 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.KeyguardState @@ -56,7 +56,7 @@ constructor( scope.launch { // Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which // otherwise would have gone through OCCLUDED first - keyguardInteractor.isDreamingWithOverlay + keyguardInteractor.isAbleToDream .sample( combine( keyguardInteractor.dozeTransitionModel, @@ -65,8 +65,7 @@ constructor( ), ::toTriple ) - .collect { triple -> - val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple + .collect { (isDreaming, dozeTransitionModel, lastStartedTransition) -> if ( !isDreaming && isDozeOff(dozeTransitionModel.to) && @@ -96,8 +95,7 @@ constructor( ), ::toTriple ) - .collect { triple -> - val (isDreaming, isOccluded, lastStartedTransition) = triple + .collect { (isDreaming, isOccluded, lastStartedTransition) -> if ( isOccluded && !isDreaming && @@ -123,24 +121,18 @@ constructor( private fun listenForDreamingToGone() { scope.launch { - keyguardInteractor.biometricUnlockState - .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) - .collect { pair -> - val (biometricUnlockState, keyguardState) = pair - if ( - keyguardState == KeyguardState.DREAMING && - isWakeAndUnlock(biometricUnlockState) - ) { - keyguardTransitionRepository.startTransition( - TransitionInfo( - name, - KeyguardState.DREAMING, - KeyguardState.GONE, - getAnimator(), - ) + keyguardInteractor.biometricUnlockState.collect { biometricUnlockState -> + if (biometricUnlockState == BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.DREAMING, + KeyguardState.GONE, + getAnimator(), ) - } + ) } + } } } @@ -151,8 +143,7 @@ constructor( keyguardTransitionInteractor.finishedKeyguardState, ::Pair ) - .collect { pair -> - val (dozeTransitionModel, keyguardState) = pair + .collect { (dozeTransitionModel, keyguardState) -> if ( dozeTransitionModel.to == DozeStateModel.DOZE && keyguardState == KeyguardState.DREAMING diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index 553fafeb92c3..9203a9b924a7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -26,7 +26,10 @@ import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton @@ -40,7 +43,7 @@ constructor( ) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) { override fun start() { - listenForGoneToAod() + listenForGoneToAodOrDozing() listenForGoneToDreaming() } @@ -56,7 +59,7 @@ constructor( name, KeyguardState.GONE, KeyguardState.DREAMING, - getAnimator(), + getAnimator(TO_DREAMING_DURATION), ) ) } @@ -64,12 +67,18 @@ constructor( } } - private fun listenForGoneToAod() { + private fun listenForGoneToAodOrDozing() { scope.launch { keyguardInteractor.wakefulnessModel - .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) - .collect { pair -> - val (wakefulnessState, keyguardState) = pair + .sample( + combine( + keyguardTransitionInteractor.finishedKeyguardState, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, keyguardState, isAodAvailable) -> if ( keyguardState == KeyguardState.GONE && wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP @@ -78,7 +87,11 @@ constructor( TransitionInfo( name, KeyguardState.GONE, - KeyguardState.AOD, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, getAnimator(), ) ) @@ -87,14 +100,15 @@ constructor( } } - private fun getAnimator(): ValueAnimator { + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) - setDuration(TRANSITION_DURATION_MS) + setDuration(duration.inWholeMilliseconds) } } companion object { - private const val TRANSITION_DURATION_MS = 500L + private val DEFAULT_DURATION = 500.milliseconds + val TO_DREAMING_DURATION = 933.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 20c6531d580b..5674e2a15271 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -21,11 +21,11 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample import java.util.UUID @@ -48,13 +48,11 @@ constructor( private val keyguardTransitionRepository: KeyguardTransitionRepository, ) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) { - private var transitionId: UUID? = null - override fun start() { listenForLockscreenToGone() listenForLockscreenToOccluded() listenForLockscreenToCamera() - listenForLockscreenToAod() + listenForLockscreenToAodOrDozing() listenForLockscreenToBouncer() listenForLockscreenToDreaming() listenForLockscreenToBouncerDragging() @@ -104,6 +102,7 @@ constructor( /* Starts transitions when manually dragging up the bouncer from the lockscreen. */ private fun listenForLockscreenToBouncerDragging() { + var transitionId: UUID? = null scope.launch { shadeRepository.shadeModel .sample( @@ -114,25 +113,43 @@ constructor( ), ::toTriple ) - .collect { triple -> - val (shadeModel, keyguardState, statusBarState) = triple - + .collect { (shadeModel, keyguardState, statusBarState) -> val id = transitionId if (id != null) { // An existing `id` means a transition is started, and calls to - // `updateTransition` will control it until FINISHED - keyguardTransitionRepository.updateTransition( - id, - 1f - shadeModel.expansionAmount, - if ( - shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f - ) { - transitionId = null + // `updateTransition` will control it until FINISHED or CANCELED + var nextState = + if (shadeModel.expansionAmount == 0f) { TransitionState.FINISHED + } else if (shadeModel.expansionAmount == 1f) { + TransitionState.CANCELED } else { TransitionState.RUNNING } + keyguardTransitionRepository.updateTransition( + id, + 1f - shadeModel.expansionAmount, + nextState, ) + + if ( + nextState == TransitionState.CANCELED || + nextState == TransitionState.FINISHED + ) { + transitionId = null + } + + // If canceled, just put the state back + if (nextState == TransitionState.CANCELED) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.BOUNCER, + to = KeyguardState.LOCKSCREEN, + animator = getAnimator(0.milliseconds) + ) + ) + } } else { // TODO (b/251849525): Remove statusbarstate check when that state is // integrated into KeyguardTransitionRepository @@ -230,19 +247,31 @@ constructor( } } - private fun listenForLockscreenToAod() { + private fun listenForLockscreenToAodOrDozing() { scope.launch { - keyguardInteractor - .dozeTransitionTo(DozeStateModel.DOZE_AOD) - .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (dozeToAod, lastStartedStep) = pair - if (lastStartedStep.to == KeyguardState.LOCKSCREEN) { + keyguardInteractor.wakefulnessModel + .sample( + combine( + keyguardTransitionInteractor.startedKeyguardTransitionStep, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, lastStartedStep, isAodAvailable) -> + if ( + lastStartedStep.to == KeyguardState.LOCKSCREEN && + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, KeyguardState.LOCKSCREEN, - KeyguardState.AOD, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, getAnimator(), ) ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index 88789019b10f..2dc8fee25379 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -23,12 +23,14 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton @@ -44,6 +46,7 @@ constructor( override fun start() { listenForOccludedToLockscreen() listenForOccludedToDreaming() + listenForOccludedToAodOrDozing() } private fun listenForOccludedToDreaming() { @@ -70,8 +73,7 @@ constructor( scope.launch { keyguardInteractor.isKeyguardOccluded .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (isOccluded, lastStartedKeyguardState) = pair + .collect { (isOccluded, lastStartedKeyguardState) -> // Occlusion signals come from the framework, and should interrupt any // existing transition if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) { @@ -88,6 +90,39 @@ constructor( } } + private fun listenForOccludedToAodOrDozing() { + scope.launch { + keyguardInteractor.wakefulnessModel + .sample( + combine( + keyguardTransitionInteractor.startedKeyguardTransitionStep, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, lastStartedStep, isAodAvailable) -> + if ( + lastStartedStep.to == KeyguardState.OCCLUDED && + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP + ) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.OCCLUDED, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, + getAnimator(), + ) + ) + } + } + } + } + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index ac2d230ee605..4cf56fe2c031 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -32,12 +32,15 @@ import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.CommandQueue.Callbacks -import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.merge /** @@ -57,6 +60,8 @@ constructor( val dozeAmount: Flow<Float> = repository.linearDozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow<Boolean> = repository.isDozing + /** Whether Always-on Display mode is available. */ + val isAodAvailable: Flow<Boolean> = repository.isAodAvailable /** Doze transition information. */ val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel /** @@ -87,15 +92,23 @@ constructor( /** * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means * that doze mode is not running and DREAMING is ok to commence. + * + * Allow a brief moment to prevent rapidly oscillating between true/false signals. */ val isAbleToDream: Flow<Boolean> = merge(isDreaming, isDreamingWithOverlay) - .sample( + .combine( dozeTransitionModel, { isDreaming, dozeTransitionModel -> isDreaming && isDozeOff(dozeTransitionModel.to) } ) + .flatMapLatest { isAbleToDream -> + flow { + delay(50) + emit(isAbleToDream) + } + } .distinctUntilChanged() /** Whether the keyguard is showing or not. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 9cdbcda1343d..ad6dbea7ae43 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -22,13 +22,17 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject +import kotlin.math.max +import kotlin.math.min import kotlin.time.Duration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter @@ -53,9 +57,16 @@ constructor( val dreamingToLockscreenTransition: Flow<TransitionStep> = repository.transition(DREAMING, LOCKSCREEN) + /** GONE->DREAMING transition information. */ + val goneToDreamingTransition: Flow<TransitionStep> = repository.transition(GONE, DREAMING) + /** LOCKSCREEN->AOD transition information. */ val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD) + /** LOCKSCREEN->BOUNCER transition information. */ + val lockscreenToBouncerTransition: Flow<TransitionStep> = + repository.transition(LOCKSCREEN, BOUNCER) + /** LOCKSCREEN->DREAMING transition information. */ val lockscreenToDreamingTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, DREAMING) @@ -106,13 +117,23 @@ constructor( ): Flow<Float> { val start = (params.startTime / totalDuration).toFloat() val chunks = (totalDuration / params.duration).toFloat() + var isRunning = false return flow - // When starting, emit a value of 0f to give animations a chance to set initial state .map { step -> + val value = (step.value - start) * chunks if (step.transitionState == STARTED) { - 0f + // When starting, make sure to always emit. If a transition is started from the + // middle, it is possible this animation is being skipped but we need to inform + // the ViewModels of the last update + isRunning = true + max(0f, min(1f, value)) + } else if (isRunning && value >= 1f) { + // Always send a final value of 1. Because of rounding, [value] may never be + // exactly 1. + isRunning = false + 1f } else { - (step.value - start) * chunks + value } } .filter { value -> value >= 0f && value <= 1f } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt index 0e4058bf8f6d..9d8bf7deb03e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -45,7 +45,6 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewMod import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.util.kotlin.pairwise import kotlin.math.pow import kotlin.math.sqrt import kotlin.time.Duration.Companion.milliseconds @@ -129,18 +128,6 @@ object KeyguardBottomAreaViewBinder { } launch { - viewModel.startButton - .map { it.isActivated } - .pairwise() - .collect { (prev, next) -> - when { - !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated) - prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated) - } - } - } - - launch { viewModel.endButton.collect { buttonModel -> updateButton( view = endButton, @@ -153,18 +140,6 @@ object KeyguardBottomAreaViewBinder { } launch { - viewModel.endButton - .map { it.isActivated } - .pairwise() - .collect { (prev, next) -> - when { - !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated) - prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated) - } - } - } - - launch { viewModel.isOverlayContainerVisible.collect { isVisible -> overlayContainer.visibility = if (isVisible) { @@ -383,6 +358,13 @@ object KeyguardBottomAreaViewBinder { .setDuration(longPressDurationMs) .withEndAction { view.setOnClickListener { + vibratorHelper?.vibrate( + if (viewModel.isActivated) { + Vibrations.Activated + } else { + Vibrations.Deactivated + } + ) viewModel.onClicked( KeyguardQuickAffordanceViewModel.OnClickedParameters( configKey = viewModel.configKey, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt index e164f5d58b07..6627865ecc79 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt @@ -22,10 +22,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge /** * Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to @@ -49,9 +53,15 @@ constructor( /** Lockscreen views y-translation */ fun lockscreenTranslationY(translatePx: Int): Flow<Float> { - return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> - -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx) - } + return merge( + flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> + -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx) + }, + // On end, reset the translation to 0 + interactor.dreamingToLockscreenTransition + .filter { it.transitionState == FINISHED || it.transitionState == CANCELED } + .map { 0f } + ) } /** Lockscreen views alpha */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt new file mode 100644 index 000000000000..5a4796096eeb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** Breaks down GONE->DREAMING transition into discrete steps for corresponding views to consume. */ +@SysUISingleton +class GoneToDreamingTransitionViewModel +@Inject +constructor( + private val interactor: KeyguardTransitionInteractor, +) { + + /** Lockscreen views y-translation */ + fun lockscreenTranslationY(translatePx: Int): Flow<Float> { + return merge( + flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> + (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx) + }, + // On end, reset the translation to 0 + interactor.goneToDreamingTransition + .filter { it.transitionState == FINISHED || it.transitionState == CANCELED } + .map { 0f } + ) + } + + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it } + + private fun flowForAnimation(params: AnimationParams): Flow<Float> { + return interactor.transitionStepAnimation( + interactor.goneToDreamingTransition, + params, + totalDuration = TO_DREAMING_DURATION + ) + } + + companion object { + val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds) + val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt index d48f87deaaf4..e05adbdab583 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt @@ -21,7 +21,8 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.AnimationParams -import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.Flow @@ -48,7 +49,7 @@ constructor( }, // On end, reset the translation to 0 interactor.lockscreenToDreamingTransition - .filter { step -> step.transitionState == TransitionState.FINISHED } + .filter { it.transitionState == FINISHED || it.transitionState == CANCELED } .map { 0f } ) } diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt index 7a90a7470cd2..7ccc43ce62c2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt @@ -29,6 +29,18 @@ constructor( private val dumpManager: DumpManager, private val systemClock: SystemClock, ) { + private val existingBuffers = mutableMapOf<String, TableLogBuffer>() + + /** + * Creates a new [TableLogBuffer]. This method should only be called from static contexts, where + * it is guaranteed only to be created one time. See [getOrCreate] for a cache-aware method of + * obtaining a buffer. + * + * @param name a unique table name + * @param maxSize the buffer max size. See [adjustMaxSize] + * + * @return a new [TableLogBuffer] registered with [DumpManager] + */ fun create( name: String, maxSize: Int, @@ -37,4 +49,23 @@ constructor( dumpManager.registerNormalDumpable(name, tableBuffer) return tableBuffer } + + /** + * Log buffers are retained indefinitely by [DumpManager], so that they can be represented in + * bugreports. Because of this, many of them are created statically in the Dagger graph. + * + * In the case where you have to create a logbuffer with a name only known at runtime, this + * method can be used to lazily create a table log buffer which is then cached for reuse. + * + * @return a [TableLogBuffer] suitable for reuse + */ + fun getOrCreate( + name: String, + maxSize: Int, + ): TableLogBuffer = + existingBuffers.getOrElse(name) { + val buffer = create(name, maxSize) + existingBuffers[name] = buffer + buffer + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt index f006442906e7..be18cbec7163 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt @@ -88,7 +88,10 @@ data class MediaData( val instanceId: InstanceId, /** The UID of the app, used for logging */ - val appUid: Int + val appUid: Int, + + /** Whether explicit indicator exists */ + val isExplicit: Boolean = false, ) { companion object { /** Media is playing on the local device */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt index a8f39fa9a456..1c8bfd1fc468 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt @@ -24,6 +24,7 @@ import android.widget.ImageView import android.widget.SeekBar import android.widget.TextView import androidx.constraintlayout.widget.Barrier +import com.android.internal.widget.CachingIconView import com.android.systemui.R import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.surfaceeffects.ripple.MultiRippleView @@ -44,6 +45,7 @@ class MediaViewHolder constructor(itemView: View) { val appIcon = itemView.requireViewById<ImageView>(R.id.icon) val titleText = itemView.requireViewById<TextView>(R.id.header_title) val artistText = itemView.requireViewById<TextView>(R.id.header_artist) + val explicitIndicator = itemView.requireViewById<CachingIconView>(R.id.media_explicit_indicator) // Output switcher val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless) @@ -123,6 +125,7 @@ class MediaViewHolder constructor(itemView: View) { R.id.app_name, R.id.header_title, R.id.header_artist, + R.id.media_explicit_indicator, R.id.media_seamless, R.id.media_progress_bar, R.id.actionPlayPause, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 2dd339d409a6..415ebeedfbde 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -45,6 +45,7 @@ import android.os.Process import android.os.UserHandle import android.provider.Settings import android.service.notification.StatusBarNotification +import android.support.v4.media.MediaMetadataCompat import android.text.TextUtils import android.util.Log import androidx.media.utils.MediaConstants @@ -660,6 +661,10 @@ class MediaDataManager( val currentEntry = mediaEntries.get(packageName) val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() val appUid = currentEntry?.appUid ?: Process.INVALID_UID + val isExplicit = + desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT && + mediaFlags.isExplicitIndicatorEnabled() val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() @@ -689,7 +694,8 @@ class MediaDataManager( hasCheckedForResume = true, lastActive = lastActive, instanceId = instanceId, - appUid = appUid + appUid = appUid, + isExplicit = isExplicit, ) ) } @@ -750,6 +756,15 @@ class MediaDataManager( song = HybridGroupManager.resolveTitle(notif) } + // Explicit Indicator + var isExplicit = false + if (mediaFlags.isExplicitIndicatorEnabled()) { + val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) + isExplicit = + mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + } + // Artist name var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) if (artist == null) { @@ -851,7 +866,8 @@ class MediaDataManager( isClearable = sbn.isClearable(), lastActive = lastActive, instanceId = instanceId, - appUid = appUid + appUid = appUid, + isExplicit = isExplicit, ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt index 899148b0014c..8f1c9048026f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt @@ -130,7 +130,12 @@ constructor( private var splitShadeContainer: ViewGroup? = null /** Track the media player setting status on lock screen. */ - private var allowMediaPlayerOnLockScreen: Boolean = true + private var allowMediaPlayerOnLockScreen: Boolean = + secureSettings.getBoolForUser( + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + true, + UserHandle.USER_CURRENT + ) private val lockScreenMediaPlayerUri = secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt index d5558b27ef1a..e7f7647797cd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt @@ -94,7 +94,7 @@ constructor( private var currentCarouselWidth: Int = 0 /** The current height of the carousel */ - private var currentCarouselHeight: Int = 0 + @VisibleForTesting var currentCarouselHeight: Int = 0 /** Are we currently showing only active players */ private var currentlyShowingOnlyActive: Boolean = false @@ -128,14 +128,14 @@ constructor( /** The measured height of the carousel */ private var carouselMeasureHeight: Int = 0 private var desiredHostState: MediaHostState? = null - private val mediaCarousel: MediaScrollView + @VisibleForTesting var mediaCarousel: MediaScrollView val mediaCarouselScrollHandler: MediaCarouselScrollHandler val mediaFrame: ViewGroup @VisibleForTesting lateinit var settingsButton: View private set private val mediaContent: ViewGroup - @VisibleForTesting val pageIndicator: PageIndicator + @VisibleForTesting var pageIndicator: PageIndicator private val visualStabilityCallback: OnReorderingAllowedListener private var needsReordering: Boolean = false private var keysNeedRemoval = mutableSetOf<String>() @@ -160,25 +160,20 @@ constructor( } companion object { - const val ANIMATION_BASE_DURATION = 2200f - const val DURATION = 167f - const val DETAILS_DELAY = 1067f - const val CONTROLS_DELAY = 1400f - const val PAGINATION_DELAY = 1900f - const val MEDIATITLES_DELAY = 1000f - const val MEDIACONTAINERS_DELAY = 967f val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F) - val REVERSE_BEZIER = PathInterpolator(0F, 0.68F, 1F, 0F) - - fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float { - val transformStartFraction = delay / ANIMATION_BASE_DURATION - val transformDurationFraction = duration / ANIMATION_BASE_DURATION - val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction) - return MathUtils.constrain( - (squishinessToTime - transformStartFraction) / transformDurationFraction, - 0F, - 1F - ) + + fun calculateAlpha( + squishinessFraction: Float, + startPosition: Float, + endPosition: Float + ): Float { + val transformFraction = + MathUtils.constrain( + (squishinessFraction - startPosition) / (endPosition - startPosition), + 0F, + 1F + ) + return TRANSFORM_BEZIER.getInterpolation(transformFraction) } } @@ -813,7 +808,12 @@ constructor( val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F val endAlpha = (if (endIsVisible) 1.0f else 0.0f) * - calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION) + calculateAlpha( + squishFraction, + (pageIndicator.translationY + pageIndicator.height) / + mediaCarousel.measuredHeight, + 1F + ) var alpha = 1.0f if (!endIsVisible || !startIsVisible) { var progress = currentTransitionProgress @@ -839,7 +839,8 @@ constructor( pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams pageIndicator.translationY = - (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat() + (mediaCarousel.measuredHeight - pageIndicator.height - layoutParams.bottomMargin) + .toFloat() } /** Update the dimension of this carousel. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 15c34430f455..f58090b2d433 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -50,7 +50,6 @@ import android.os.Process; import android.os.Trace; import android.text.TextUtils; import android.util.Log; -import android.util.Pair; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -68,6 +67,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; +import com.android.internal.widget.CachingIconView; import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.R; @@ -113,6 +113,8 @@ import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.time.SystemClock; +import dagger.Lazy; + import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; @@ -120,7 +122,7 @@ import java.util.concurrent.Executor; import javax.inject.Inject; -import dagger.Lazy; +import kotlin.Triple; import kotlin.Unit; /** @@ -398,10 +400,11 @@ public class MediaControlPanel { TextView titleText = mMediaViewHolder.getTitleText(); TextView artistText = mMediaViewHolder.getArtistText(); + CachingIconView explicitIndicator = mMediaViewHolder.getExplicitIndicator(); AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter, - Interpolators.EMPHASIZED_DECELERATE, titleText, artistText); + Interpolators.EMPHASIZED_DECELERATE, titleText, artistText, explicitIndicator); AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit, - Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText); + Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText, explicitIndicator); MultiRippleView multiRippleView = vh.getMultiRippleView(); mMultiRippleController = new MultiRippleController(multiRippleView); @@ -664,11 +667,15 @@ public class MediaControlPanel { private boolean bindSongMetadata(MediaData data) { TextView titleText = mMediaViewHolder.getTitleText(); TextView artistText = mMediaViewHolder.getArtistText(); + ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); + ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); return mMetadataAnimationHandler.setNext( - Pair.create(data.getSong(), data.getArtist()), + new Triple(data.getSong(), data.getArtist(), data.isExplicit()), () -> { titleText.setText(data.getSong()); artistText.setText(data.getArtist()); + setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator, data.isExplicit()); + setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator, data.isExplicit()); // refreshState is required here to resize the text views (and prevent ellipsis) mMediaViewController.refreshState(); diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index f7a9bc760caf..66f12d6242b0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -41,6 +41,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeStateEvents @@ -93,6 +94,7 @@ constructor( private val keyguardStateController: KeyguardStateController, private val bypassController: KeyguardBypassController, private val mediaCarouselController: MediaCarouselController, + private val mediaManager: MediaDataManager, private val keyguardViewController: KeyguardViewController, private val dreamOverlayStateController: DreamOverlayStateController, configurationController: ConfigurationController, @@ -224,9 +226,9 @@ constructor( private var inSplitShade = false - /** Is there any active media in the carousel? */ - private var hasActiveMedia: Boolean = false - get() = mediaHosts.get(LOCATION_QQS)?.visible == true + /** Is there any active media or recommendation in the carousel? */ + private var hasActiveMediaOrRecommendation: Boolean = false + get() = mediaManager.hasActiveMediaOrRecommendation() /** Are we currently waiting on an animation to start? */ private var animationPending: Boolean = false @@ -582,12 +584,8 @@ constructor( val viewHost = createUniqueObjectHost() mediaObject.hostView = viewHost mediaObject.addVisibilityChangeListener { - // If QQS changes visibility, we need to force an update to ensure the transition - // goes into the correct state - val stateUpdate = mediaObject.location == LOCATION_QQS - // Never animate because of a visibility change, only state changes should do that - updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate) + updateDesiredLocation(forceNoAnimation = true) } mediaHosts[mediaObject.location] = mediaObject if (mediaObject.location == desiredLocation) { @@ -908,7 +906,7 @@ constructor( fun isCurrentlyInGuidedTransformation(): Boolean { return hasValidStartAndEndLocations() && getTransformationProgress() >= 0 && - areGuidedTransitionHostsVisible() + (areGuidedTransitionHostsVisible() || !hasActiveMediaOrRecommendation) } private fun hasValidStartAndEndLocations(): Boolean { @@ -965,7 +963,7 @@ constructor( private fun getQSTransformationProgress(): Float { val currentHost = getHost(desiredLocation) val previousHost = getHost(previousLocation) - if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) { + if (currentHost?.location == LOCATION_QS && !inSplitShade) { if (previousHost?.location == LOCATION_QQS) { if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) { return qsExpansion @@ -1028,7 +1026,8 @@ constructor( private fun updateHostAttachment() = traceSection("MediaHierarchyManager#updateHostAttachment") { var newLocation = resolveLocationForFading() - var canUseOverlay = !isCurrentlyFading() + // Don't use the overlay when fading or when we don't have active media + var canUseOverlay = !isCurrentlyFading() && hasActiveMediaOrRecommendation if (isCrossFadeAnimatorRunning) { if ( getHost(newLocation)?.visible == true && @@ -1122,7 +1121,6 @@ constructor( dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS qsExpansion > 0.4f && onLockscreen -> LOCATION_QS - !hasActiveMedia -> LOCATION_QS onLockscreen && isSplitShadeExpanding() -> LOCATION_QS onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index 322421318cb8..2ec7be6eaa32 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -24,11 +24,6 @@ import com.android.systemui.R import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.animation.MeasurementOutput @@ -36,6 +31,8 @@ import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionLayoutController import com.android.systemui.util.animation.TransitionViewState import com.android.systemui.util.traceSection +import java.lang.Float.max +import java.lang.Float.min import javax.inject.Inject /** @@ -80,6 +77,7 @@ constructor( setOf( R.id.header_title, R.id.header_artist, + R.id.media_explicit_indicator, R.id.actionPlayPause, ) @@ -304,39 +302,106 @@ constructor( val squishedViewState = viewState.copy() val squishedHeight = (squishedViewState.measureHeight * squishFraction).toInt() squishedViewState.height = squishedHeight - controlIds.forEach { id -> - squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION) - } - } - - detailIds.forEach { id -> - squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION) - } - } - // We are not overriding the squishedViewStates height but only the children to avoid // them remeasuring the whole view. Instead it just remains as the original size backgroundIds.forEach { id -> - squishedViewState.widgetStates.get(id)?.let { state -> - state.height = squishedHeight - } + squishedViewState.widgetStates.get(id)?.let { state -> state.height = squishedHeight } } - RecommendationViewHolder.mediaContainersIds.forEach { id -> + // media player + val controlsTop = + calculateWidgetGroupAlphaForSquishiness( + controlIds, + squishedViewState.measureHeight.toFloat(), + squishedViewState, + squishFraction + ) + calculateWidgetGroupAlphaForSquishiness( + detailIds, + controlsTop, + squishedViewState, + squishFraction + ) + // recommendation card + val titlesTop = + calculateWidgetGroupAlphaForSquishiness( + RecommendationViewHolder.mediaTitlesAndSubtitlesIds, + squishedViewState.measureHeight.toFloat(), + squishedViewState, + squishFraction + ) + calculateWidgetGroupAlphaForSquishiness( + RecommendationViewHolder.mediaContainersIds, + titlesTop, + squishedViewState, + squishFraction + ) + return squishedViewState + } + + /** + * This function is to make each widget in UMO disappear before being clipped by squished UMO + * + * The general rule is that widgets in UMO has been divided into several groups, and widgets in + * one group have the same alpha during squishing It will change from alpha 0.0 when the visible + * bottom of UMO reach the bottom of this group It will change to alpha 1.0 when the visible + * bottom of UMO reach the top of the group below e.g.Album title, artist title and play-pause + * button will change alpha together. + * ``` + * And their alpha becomes 1.0 when the visible bottom of UMO reach the top of controls, + * including progress bar, next button, previous button + * ``` + * widgetGroupIds: a group of widgets have same state during UMO is squished, + * ``` + * e.g. Album title, artist title and play-pause button + * ``` + * groupEndPosition: the height of UMO, when the height reaches this value, + * ``` + * widgets in this group should have 1.0 as alpha + * e.g., the group of album title, artist title and play-pause button will become fully + * visible when the height of UMO reaches the top of controls group + * (progress bar, previous button and next button) + * ``` + * squishedViewState: hold the widgetState of each widget, which will be modified + * squishFraction: the squishFraction of UMO + */ + private fun calculateWidgetGroupAlphaForSquishiness( + widgetGroupIds: Set<Int>, + groupEndPosition: Float, + squishedViewState: TransitionViewState, + squishFraction: Float + ): Float { + val nonsquishedHeight = squishedViewState.measureHeight + var groupTop = squishedViewState.measureHeight.toFloat() + var groupBottom = 0F + widgetGroupIds.forEach { id -> squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION) + groupTop = min(groupTop, state.y) + groupBottom = max(groupBottom, state.y + state.height) } } - - RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id -> + // startPosition means to the height of squished UMO where the widget alpha should start + // changing from 0.0 + // generally, it equals to the bottom of widgets, so that we can meet the requirement that + // widget should not go beyond the bounds of background + // endPosition means to the height of squished UMO where the widget alpha should finish + // changing alpha to 1.0 + var startPosition = groupBottom + val endPosition = groupEndPosition + if (startPosition == endPosition) { + startPosition = (endPosition - 0.2 * (groupBottom - groupTop)).toFloat() + } + widgetGroupIds.forEach { id -> squishedViewState.widgetStates.get(id)?.let { state -> - state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION) + state.alpha = + calculateAlpha( + squishFraction, + startPosition / nonsquishedHeight, + endPosition / nonsquishedHeight + ) } } - - return squishedViewState + return groupTop // used for the widget group above this group } /** @@ -544,11 +609,13 @@ constructor( overrideSize?.let { // To be safe we're using a maximum here. The override size should always be set // properly though. - if (result.measureHeight != it.measuredHeight - || result.measureWidth != it.measuredWidth) { + if ( + result.measureHeight != it.measuredHeight || result.measureWidth != it.measuredWidth + ) { result.measureHeight = Math.max(it.measuredHeight, result.measureHeight) result.measureWidth = Math.max(it.measuredWidth, result.measureWidth) - // The measureHeight and the shown height should both be set to the overridden height + // The measureHeight and the shown height should both be set to the overridden + // height result.height = result.measureHeight result.width = result.measureWidth // Make sure all background views are also resized such that their size is correct diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 8d4931a5d08c..5bc35caed515 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -42,4 +42,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information. */ fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES) + + /** Check whether we show explicit indicator on UMO */ + fun isExplicitIndicatorEnabled() = featureFlags.isEnabled(Flags.MEDIA_EXPLICIT_INDICATOR) } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index 9f44d984124f..935f38de2e4f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -150,7 +150,12 @@ constructor( logger: MediaTttLogger<ChipbarInfo>, ): ChipbarInfo { val packageName = routeInfo.clientPackageName - val otherDeviceName = routeInfo.name.toString() + val otherDeviceName = + if (routeInfo.name.isBlank()) { + context.getString(R.string.media_ttt_default_device_type) + } else { + routeInfo.name.toString() + } return ChipbarInfo( // Display the app's icon as the start icon diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 5b306c98912d..2647600e8684 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -16,6 +16,7 @@ package com.android.systemui.navigationbar; +import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; @@ -44,6 +45,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode; import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE; import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode; @@ -136,9 +138,10 @@ import com.android.systemui.shared.navigationbar.RegionSamplingHelper; import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.shared.rotation.RotationButton; import com.android.systemui.shared.rotation.RotationButtonController; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.SysUiStatsLog; +import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.AutoHideUiElement; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CommandQueue.Callbacks; @@ -252,6 +255,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final AutoHideController.Factory mAutoHideControllerFactory; private final Optional<TelecomManager> mTelecomManagerOptional; private final InputMethodManager mInputMethodManager; + private final TaskStackChangeListeners mTaskStackChangeListeners; @VisibleForTesting public int mDisplayId; @@ -488,6 +492,18 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } }; + private boolean mScreenPinningActive = false; + private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { + @Override + public void onLockTaskModeChanged(int mode) { + mScreenPinningActive = (mode == LOCK_TASK_MODE_PINNED); + mSysUiFlagsContainer.setFlag(SYSUI_STATE_SCREEN_PINNING, mScreenPinningActive) + .commitUpdate(mDisplayId); + mView.setInScreenPinning(mScreenPinningActive); + updateScreenPinningGestures(); + } + }; + @Inject NavigationBar( NavigationBarView navigationBarView, @@ -529,7 +545,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements EdgeBackGestureHandler edgeBackGestureHandler, Optional<BackAnimation> backAnimation, UserContextProvider userContextProvider, - WakefulnessLifecycle wakefulnessLifecycle) { + WakefulnessLifecycle wakefulnessLifecycle, + TaskStackChangeListeners taskStackChangeListeners) { super(navigationBarView); mFrame = navigationBarFrame; mContext = context; @@ -568,6 +585,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mInputMethodManager = inputMethodManager; mUserContextProvider = userContextProvider; mWakefulnessLifecycle = wakefulnessLifecycle; + mTaskStackChangeListeners = taskStackChangeListeners; mNavColorSampleMargin = getResources() .getDimensionPixelSize(R.dimen.navigation_handle_sample_horizontal_margin); @@ -676,6 +694,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mCommandQueue.recomputeDisableFlags(mDisplayId, false); mNotificationShadeDepthController.addListener(mDepthListener); + mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener); } public void destroyView() { @@ -689,6 +708,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mNotificationShadeDepthController.removeListener(mDepthListener); mDeviceConfigProxy.removeOnPropertiesChangedListener(mOnPropertiesChangedListener); + mTaskStackChangeListeners.unregisterTaskStackListener(mTaskStackListener); } @Override @@ -990,6 +1010,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements pw.println(" mTransientShown=" + mTransientShown); pw.println(" mTransientShownFromGestureOnSystemBar=" + mTransientShownFromGestureOnSystemBar); + pw.println(" mScreenPinningActive=" + mScreenPinningActive); dumpBarTransitions(pw, "mNavigationBarView", getBarTransitions()); pw.println(" mOrientedHandleSamplingRegion: " + mOrientedHandleSamplingRegion); @@ -1213,10 +1234,9 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private void updateScreenPinningGestures() { // Change the cancel pin gesture to home and back if recents button is invisible - boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive(); ButtonDispatcher backButton = mView.getBackButton(); ButtonDispatcher recentsButton = mView.getRecentsButton(); - if (pinningActive) { + if (mScreenPinningActive) { boolean recentsVisible = mView.isRecentsButtonVisible(); backButton.setOnLongClickListener(recentsVisible ? this::onLongPressBackRecents @@ -1227,8 +1247,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements recentsButton.setOnLongClickListener(null); } // Note, this needs to be set after even if we're setting the listener to null - backButton.setLongClickable(pinningActive); - recentsButton.setLongClickable(pinningActive); + backButton.setLongClickable(mScreenPinningActive); + recentsButton.setLongClickable(mScreenPinningActive); } private void notifyNavigationBarScreenOn() { @@ -1311,8 +1331,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements @VisibleForTesting boolean onHomeLongClick(View v) { - if (!mView.isRecentsButtonVisible() - && ActivityManagerWrapper.getInstance().isScreenPinningActive()) { + if (!mView.isRecentsButtonVisible() && mScreenPinningActive) { return onLongPressBackHome(v); } if (shouldDisableNavbarGestures()) { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java index 891455249867..5d43c5dc19fa 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java @@ -57,6 +57,7 @@ import com.android.systemui.flags.Flags; import com.android.systemui.model.SysUiState; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.shared.system.QuickStepContract; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.CommandQueue.Callbacks; import com.android.systemui.statusbar.phone.AutoHideController; @@ -88,7 +89,6 @@ public class NavigationBarController implements private FeatureFlags mFeatureFlags; private final DisplayManager mDisplayManager; private final TaskbarDelegate mTaskbarDelegate; - private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private int mNavMode; @VisibleForTesting boolean mIsTablet; @@ -112,10 +112,10 @@ public class NavigationBarController implements NavBarHelper navBarHelper, TaskbarDelegate taskbarDelegate, NavigationBarComponent.Factory navigationBarComponentFactory, - StatusBarKeyguardViewManager statusBarKeyguardViewManager, DumpManager dumpManager, AutoHideController autoHideController, LightBarController lightBarController, + TaskStackChangeListeners taskStackChangeListeners, Optional<Pip> pipOptional, Optional<BackAnimation> backAnimation, FeatureFlags featureFlags) { @@ -129,11 +129,10 @@ public class NavigationBarController implements mConfigChanges.applyNewConfig(mContext.getResources()); mNavMode = navigationModeController.addListener(this); mTaskbarDelegate = taskbarDelegate; - mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mTaskbarDelegate.setDependencies(commandQueue, overviewProxyService, navBarHelper, navigationModeController, sysUiFlagsContainer, dumpManager, autoHideController, lightBarController, pipOptional, - backAnimation.orElse(null)); + backAnimation.orElse(null), taskStackChangeListeners); mIsTablet = isTablet(mContext); dumpManager.registerDumpable(this); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java index 403d276f8cbc..88c4fd524b79 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java @@ -16,6 +16,7 @@ package com.android.systemui.navigationbar; +import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; import static android.inputmethodservice.InputMethodService.canImeRenderGesturalNavButtons; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; @@ -80,6 +81,7 @@ import com.android.systemui.shared.rotation.RotationButton.RotationButtonUpdates import com.android.systemui.shared.rotation.RotationButtonController; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; +import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.statusbar.phone.AutoHideController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.LightBarTransitionsController; @@ -160,6 +162,7 @@ public class NavigationBarView extends FrameLayout { * fully locked mode we only show that unlocking is blocked. */ private ScreenPinningNotify mScreenPinningNotify; + private boolean mScreenPinningActive = false; /** * {@code true} if the IME can render the back button and the IME switcher button. @@ -636,14 +639,13 @@ public class NavigationBarView extends FrameLayout { // When screen pinning, don't hide back and home when connected service or back and // recents buttons when disconnected from launcher service in screen pinning mode, // as they are used for exiting. - final boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive(); if (mOverviewProxyEnabled) { // Force disable recents when not in legacy mode disableRecent |= !QuickStepContract.isLegacyMode(mNavBarMode); - if (pinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) { + if (mScreenPinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) { disableBack = disableHome = false; } - } else if (pinningActive) { + } else if (mScreenPinningActive) { disableBack = disableRecent = false; } @@ -738,9 +740,7 @@ public class NavigationBarView extends FrameLayout { public void updateDisabledSystemUiStateFlags(SysUiState sysUiState) { int displayId = mContext.getDisplayId(); - sysUiState.setFlag(SYSUI_STATE_SCREEN_PINNING, - ActivityManagerWrapper.getInstance().isScreenPinningActive()) - .setFlag(SYSUI_STATE_OVERVIEW_DISABLED, + sysUiState.setFlag(SYSUI_STATE_OVERVIEW_DISABLED, (mDisabledFlags & View.STATUS_BAR_DISABLE_RECENT) != 0) .setFlag(SYSUI_STATE_HOME_DISABLED, (mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0) @@ -749,6 +749,10 @@ public class NavigationBarView extends FrameLayout { .commitUpdate(displayId); } + public void setInScreenPinning(boolean active) { + mScreenPinningActive = active; + } + private void updatePanelSystemUiStateFlags() { if (SysUiState.DEBUG) { Log.d(TAG, "Updating panel sysui state flags: panelView=" + mPanelView); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index 5e26e6050eaa..6ee86aa021a3 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -16,6 +16,7 @@ package com.android.systemui.navigationbar; +import static android.app.ActivityManager.LOCK_TASK_MODE_PINNED; import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT; import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; @@ -40,6 +41,7 @@ import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode import android.app.StatusBarManager; import android.app.StatusBarManager.WindowVisibleState; +import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; @@ -68,6 +70,8 @@ import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; +import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.AutoHideUiElement; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.AutoHideController; @@ -101,6 +105,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, private AutoHideController mAutoHideController; private LightBarController mLightBarController; private LightBarTransitionsController mLightBarTransitionsController; + private TaskStackChangeListeners mTaskStackChangeListeners; private Optional<Pip> mPipOptional; private int mDisplayId; private int mNavigationIconHints; @@ -127,6 +132,14 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, private final DisplayManager mDisplayManager; private Context mWindowContext; private ScreenPinningNotify mScreenPinningNotify; + private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { + @Override + public void onLockTaskModeChanged(int mode) { + mSysUiState.setFlag(SYSUI_STATE_SCREEN_PINNING, mode == LOCK_TASK_MODE_PINNED) + .commitUpdate(mDisplayId); + } + }; + private int mNavigationMode = -1; private final Consumer<Rect> mPipListener; @@ -176,7 +189,8 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, AutoHideController autoHideController, LightBarController lightBarController, Optional<Pip> pipOptional, - BackAnimation backAnimation) { + BackAnimation backAnimation, + TaskStackChangeListeners taskStackChangeListeners) { // TODO: adding this in the ctor results in a dagger dependency cycle :( mCommandQueue = commandQueue; mOverviewProxyService = overviewProxyService; @@ -189,6 +203,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, mPipOptional = pipOptional; mBackAnimation = backAnimation; mLightBarTransitionsController = createLightBarTransitionsController(); + mTaskStackChangeListeners = taskStackChangeListeners; } // Separated into a method to keep setDependencies() clean/readable. @@ -234,6 +249,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, mPipOptional.ifPresent(this::addPipExclusionBoundsChangeListener); mEdgeBackGestureHandler.setBackAnimation(mBackAnimation); mEdgeBackGestureHandler.onConfigurationChanged(mContext.getResources().getConfiguration()); + mTaskStackChangeListeners.registerTaskStackListener(mTaskStackListener); mInitialized = true; } @@ -253,6 +269,7 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, mLightBarTransitionsController.destroy(); mLightBarController.setNavigationBar(null); mPipOptional.ifPresent(this::removePipExclusionBoundsChangeListener); + mTaskStackChangeListeners.unregisterTaskStackListener(mTaskStackListener); mInitialized = false; } @@ -300,8 +317,6 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, .setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isWindowVisible()) .setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY, allowSystemGestureIgnoringBarVisibility()) - .setFlag(SYSUI_STATE_SCREEN_PINNING, - ActivityManagerWrapper.getInstance().isScreenPinningActive()) .setFlag(SYSUI_STATE_IMMERSIVE_MODE, isImmersiveMode()) .commitUpdate(mDisplayId); } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 6dd60d043a06..08d18575da79 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -57,7 +57,9 @@ constructor( * If the keyguard is locked, notes will open as a full screen experience. A locked device has * no contextual information which let us use the whole screen space available. * - * If no in multi-window or the keyguard is unlocked, notes will open as a floating experience. + * If no in multi-window or the keyguard is unlocked, notes will open as a bubble OR it will be + * collapsed if the notes bubble is already opened. + * * That will let users open other apps in full screen, and take contextual notes. */ fun showNoteTask(isInMultiWindowMode: Boolean = false) { @@ -75,7 +77,7 @@ constructor( context.startActivity(intent) } else { // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter. - bubbles.showAppBubble(intent) + bubbles.showOrHideAppBubble(intent) } } @@ -102,4 +104,9 @@ constructor( PackageManager.DONT_KILL_APP, ) } + + companion object { + // TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead. + const val NOTE_TASK_KEY_EVENT = 311 + } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt index d14b7a766762..d5f4a5a5d351 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -16,7 +16,6 @@ package com.android.systemui.notetask -import android.view.KeyEvent import androidx.annotation.VisibleForTesting import com.android.systemui.statusbar.CommandQueue import com.android.wm.shell.bubbles.Bubbles @@ -37,7 +36,7 @@ constructor( val callbacks = object : CommandQueue.Callbacks { override fun handleSystemKey(keyCode: Int) { - if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) { + if (keyCode == NoteTaskController.NOTE_TASK_KEY_EVENT) { noteTaskController.showNoteTask() } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt index 98d69910aac3..26e3f49828c7 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt @@ -21,12 +21,12 @@ import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags -import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE import javax.inject.Inject /** - * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an - * [Intent] ready for be launched will be returned. Otherwise, returns null. + * Class responsible to query all apps and find one that can handle the [ACTION_CREATE_NOTE]. If + * found, an [Intent] ready for be launched will be returned. Otherwise, returns null. * * TODO(b/248274123): should be revisited once the notes role is implemented. */ @@ -37,15 +37,16 @@ constructor( ) { fun resolveIntent(): Intent? { - val intent = Intent(NOTES_ACTION) + val intent = Intent(ACTION_CREATE_NOTE) val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) val infoList = packageManager.queryIntentActivities(intent, flags) for (info in infoList) { - val packageName = info.serviceInfo.applicationInfo.packageName ?: continue + val packageName = info.activityInfo.applicationInfo.packageName ?: continue val activityName = resolveActivityNameForNotesAction(packageName) ?: continue - return Intent(NOTES_ACTION) + return Intent(ACTION_CREATE_NOTE) + .setPackage(packageName) .setComponent(ComponentName(packageName, activityName)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } @@ -54,7 +55,7 @@ constructor( } private fun resolveActivityNameForNotesAction(packageName: String): String? { - val intent = Intent(NOTES_ACTION).setPackage(packageName) + val intent = Intent(ACTION_CREATE_NOTE).setPackage(packageName) val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) val resolveInfo = packageManager.resolveActivity(intent, flags) @@ -69,8 +70,8 @@ constructor( } companion object { - // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead. - const val NOTES_ACTION = "android.intent.action.NOTES" + // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead. + const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE" } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt index 47fe67638cd0..f203e7a51643 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/shortcut/LaunchNoteTaskActivity.kt @@ -45,8 +45,8 @@ constructor( fun newIntent(context: Context): Intent { return Intent(context, LaunchNoteTaskActivity::class.java).apply { // Intent's action must be set in shortcuts, or an exception will be thrown. - // TODO(b/254606432): Use Intent.ACTION_NOTES instead. - action = NoteTaskIntentResolver.NOTES_ACTION + // TODO(b/254606432): Use Intent.ACTION_CREATE_NOTE instead. + action = NoteTaskIntentResolver.ACTION_CREATE_NOTE } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index f49ffb4d930c..774cb3442811 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -68,6 +68,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.util.LifecycleFragment; +import com.android.systemui.util.Utils; import java.io.PrintWriter; import java.util.Arrays; @@ -683,7 +684,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } else { mQsMediaHost.setSquishFraction(mSquishinessFraction); } - + updateMediaPositions(); } private void setAlphaAnimationProgress(float progress) { @@ -758,6 +759,22 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca - mQSPanelController.getPaddingBottom()); } + private void updateMediaPositions() { + if (Utils.useQsMediaPlayer(getContext())) { + View hostView = mQsMediaHost.getHostView(); + // Make sure the media appears a bit from the top to make it look nicer + if (mLastQSExpansion > 0 && !isKeyguardState() && !mQqsMediaHost.getVisible() + && !mQSPanelController.shouldUseHorizontalLayout() && !mInSplitShade) { + float interpolation = 1.0f - mLastQSExpansion; + interpolation = Interpolators.ACCELERATE.getInterpolation(interpolation); + float translationY = -hostView.getHeight() * 1.3f * interpolation; + hostView.setTranslationY(translationY); + } else { + hostView.setTranslationY(0); + } + } + } + private boolean headerWillBeAnimating() { return mStatusBarState == KEYGUARD && mShowCollapsedOnKeyguard && !isKeyguardState(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index 7cf63f678c1d..1da30ade951b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -36,7 +36,6 @@ public interface QSHost { void removeCallback(Callback callback); void removeTile(String tileSpec); void removeTiles(Collection<String> specs); - void unmarkTileAsAutoAdded(String tileSpec); int indexOf(String tileSpec); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 7bb672ce5880..e85d0a32cfa8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -372,18 +372,18 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr if (mUsingHorizontalLayout) { // Only height remaining parameters.getDisappearSize().set(0.0f, 0.4f); - // Disappearing on the right side on the bottom - parameters.getGonePivot().set(1.0f, 1.0f); + // Disappearing on the right side on the top + parameters.getGonePivot().set(1.0f, 0.0f); // translating a bit horizontal parameters.getContentTranslationFraction().set(0.25f, 1.0f); parameters.setDisappearEnd(0.6f); } else { // Only width remaining parameters.getDisappearSize().set(1.0f, 0.0f); - // Disappearing on the bottom - parameters.getGonePivot().set(0.0f, 1.0f); + // Disappearing on the top + parameters.getGonePivot().set(0.0f, 0.0f); // translating a bit vertical - parameters.getContentTranslationFraction().set(0.0f, 1.05f); + parameters.getContentTranslationFraction().set(0.0f, 1f); parameters.setDisappearEnd(0.95f); } parameters.setFadeStartPosition(0.95f); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index cad296b671b3..100853caa2d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -427,11 +427,6 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P mMainExecutor.execute(() -> changeTileSpecs(tileSpecs -> tileSpecs.removeAll(specs))); } - @Override - public void unmarkTileAsAutoAdded(String spec) { - if (mAutoTiles != null) mAutoTiles.unmarkTileAsAutoAdded(spec); - } - /** * Add a tile to the end * diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt index 30f81243e8d0..19215867e678 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt @@ -219,9 +219,9 @@ object FooterActionsViewBinder { // Small button with the number only. foregroundServicesWithTextView.isVisible = false - foregroundServicesWithNumberView.visibility = View.VISIBLE + foregroundServicesWithNumberView.isVisible = true foregroundServicesWithNumberView.setOnClickListener { - foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView)) + foregroundServices.onClick(Expandable.fromView(foregroundServicesWithNumberView)) } foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString() foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt index 9f376ae75efe..d32ef327e90e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt @@ -49,109 +49,135 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : } fun logTileAdded(tileSpec: String) { - log(DEBUG, { - str1 = tileSpec - }, { - "[$str1] Tile added" - }) + buffer.log(TAG, DEBUG, { str1 = tileSpec }, { "[$str1] Tile added" }) } fun logTileDestroyed(tileSpec: String, reason: String) { - log(DEBUG, { - str1 = tileSpec - str2 = reason - }, { - "[$str1] Tile destroyed. Reason: $str2" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + str2 = reason + }, + { "[$str1] Tile destroyed. Reason: $str2" } + ) } fun logTileChangeListening(tileSpec: String, listening: Boolean) { - log(VERBOSE, { - bool1 = listening - str1 = tileSpec - }, { - "[$str1] Tile listening=$bool1" - }) + buffer.log( + TAG, + VERBOSE, + { + bool1 = listening + str1 = tileSpec + }, + { "[$str1] Tile listening=$bool1" } + ) } fun logAllTilesChangeListening(listening: Boolean, containerName: String, allSpecs: String) { - log(DEBUG, { - bool1 = listening - str1 = containerName - str2 = allSpecs - }, { - "Tiles listening=$bool1 in $str1. $str2" - }) + buffer.log( + TAG, + DEBUG, + { + bool1 = listening + str1 = containerName + str2 = allSpecs + }, + { "Tiles listening=$bool1 in $str1. $str2" } + ) } fun logTileClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - str2 = StatusBarState.toString(statusBarState) - str3 = toStateString(state) - }, { - "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + str2 = StatusBarState.toString(statusBarState) + str3 = toStateString(state) + }, + { "[$str1][$int1] Tile clicked. StatusBarState=$str2. TileState=$str3" } + ) } fun logHandleClick(tileSpec: String, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - }, { - "[$str1][$int1] Tile handling click." - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + }, + { "[$str1][$int1] Tile handling click." } + ) } fun logTileSecondaryClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - str2 = StatusBarState.toString(statusBarState) - str3 = toStateString(state) - }, { - "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + str2 = StatusBarState.toString(statusBarState) + str3 = toStateString(state) + }, + { "[$str1][$int1] Tile secondary clicked. StatusBarState=$str2. TileState=$str3" } + ) } fun logHandleSecondaryClick(tileSpec: String, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - }, { - "[$str1][$int1] Tile handling secondary click." - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + }, + { "[$str1][$int1] Tile handling secondary click." } + ) } fun logTileLongClick(tileSpec: String, statusBarState: Int, state: Int, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - str2 = StatusBarState.toString(statusBarState) - str3 = toStateString(state) - }, { - "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + str2 = StatusBarState.toString(statusBarState) + str3 = toStateString(state) + }, + { "[$str1][$int1] Tile long clicked. StatusBarState=$str2. TileState=$str3" } + ) } fun logHandleLongClick(tileSpec: String, eventId: Int) { - log(DEBUG, { - str1 = tileSpec - int1 = eventId - }, { - "[$str1][$int1] Tile handling long click." - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileSpec + int1 = eventId + }, + { "[$str1][$int1] Tile handling long click." } + ) } fun logInternetTileUpdate(tileSpec: String, lastType: Int, callback: String) { - log(VERBOSE, { - str1 = tileSpec - int1 = lastType - str2 = callback - }, { - "[$str1] mLastTileState=$int1, Callback=$str2." - }) + buffer.log( + TAG, + VERBOSE, + { + str1 = tileSpec + int1 = lastType + str2 = callback + }, + { "[$str1] mLastTileState=$int1, Callback=$str2." } + ) } // TODO(b/250618218): Remove this method once we know the root cause of b/250618218. @@ -167,58 +193,75 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : if (tileSpec != "internet") { return } - log(VERBOSE, { - str1 = tileSpec - int1 = state - bool1 = disabledByPolicy - int2 = color - }, { - "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." - }) + buffer.log( + TAG, + VERBOSE, + { + str1 = tileSpec + int1 = state + bool1 = disabledByPolicy + int2 = color + }, + { "[$str1] state=$int1, disabledByPolicy=$bool1, color=$int2." } + ) } fun logTileUpdated(tileSpec: String, state: QSTile.State) { - log(VERBOSE, { - str1 = tileSpec - str2 = state.label?.toString() - str3 = state.icon?.toString() - int1 = state.state - if (state is QSTile.SignalState) { - bool1 = true - bool2 = state.activityIn - bool3 = state.activityOut + buffer.log( + TAG, + VERBOSE, + { + str1 = tileSpec + str2 = state.label?.toString() + str3 = state.icon?.toString() + int1 = state.state + if (state is QSTile.SignalState) { + bool1 = true + bool2 = state.activityIn + bool3 = state.activityOut + } + }, + { + "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." + + if (bool1) " Activity in/out=$bool2/$bool3" else "" } - }, { - "[$str1] Tile updated. Label=$str2. State=$int1. Icon=$str3." + - if (bool1) " Activity in/out=$bool2/$bool3" else "" - }) + ) } fun logPanelExpanded(expanded: Boolean, containerName: String) { - log(DEBUG, { - str1 = containerName - bool1 = expanded - }, { - "$str1 expanded=$bool1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + bool1 = expanded + }, + { "$str1 expanded=$bool1" } + ) } fun logOnViewAttached(orientation: Int, containerName: String) { - log(DEBUG, { - str1 = containerName - int1 = orientation - }, { - "onViewAttached: $str1 orientation $int1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + int1 = orientation + }, + { "onViewAttached: $str1 orientation $int1" } + ) } fun logOnViewDetached(orientation: Int, containerName: String) { - log(DEBUG, { - str1 = containerName - int1 = orientation - }, { - "onViewDetached: $str1 orientation $int1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + int1 = orientation + }, + { "onViewDetached: $str1 orientation $int1" } + ) } fun logOnConfigurationChanged( @@ -226,13 +269,16 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : newOrientation: Int, containerName: String ) { - log(DEBUG, { - str1 = containerName - int1 = lastOrientation - int2 = newOrientation - }, { - "configuration change: $str1 orientation was $int1, now $int2" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + int1 = lastOrientation + int2 = newOrientation + }, + { "configuration change: $str1 orientation was $int1, now $int2" } + ) } fun logSwitchTileLayout( @@ -241,32 +287,41 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : force: Boolean, containerName: String ) { - log(DEBUG, { - str1 = containerName - bool1 = after - bool2 = before - bool3 = force - }, { - "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = containerName + bool1 = after + bool2 = before + bool3 = force + }, + { "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3" } + ) } fun logTileDistributionInProgress(tilesPerPageCount: Int, totalTilesCount: Int) { - log(DEBUG, { - int1 = tilesPerPageCount - int2 = totalTilesCount - }, { - "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" - }) + buffer.log( + TAG, + DEBUG, + { + int1 = tilesPerPageCount + int2 = totalTilesCount + }, + { "Distributing tiles: [tilesPerPageCount=$int1] [totalTilesCount=$int2]" } + ) } fun logTileDistributed(tileName: String, pageIndex: Int) { - log(DEBUG, { - str1 = tileName - int1 = pageIndex - }, { - "Adding $str1 to page number $int1" - }) + buffer.log( + TAG, + DEBUG, + { + str1 = tileName + int1 = pageIndex + }, + { "Adding $str1 to page number $int1" } + ) } private fun toStateString(state: Int): String { @@ -277,12 +332,4 @@ class QSLogger @Inject constructor(@QSLog private val buffer: LogBuffer) : else -> "wrong state" } } - - private inline fun log( - logLevel: LogLevel, - initializer: LogMessage.() -> Unit, - noinline printer: LogMessage.() -> String - ) { - buffer.log(TAG, logLevel, initializer, printer) - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java index a92c7e3c8554..24a4f60b7c00 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -33,7 +33,6 @@ import com.android.systemui.qs.tiles.BatterySaverTile; import com.android.systemui.qs.tiles.BluetoothTile; import com.android.systemui.qs.tiles.CameraToggleTile; import com.android.systemui.qs.tiles.CastTile; -import com.android.systemui.qs.tiles.CellularTile; import com.android.systemui.qs.tiles.ColorCorrectionTile; import com.android.systemui.qs.tiles.ColorInversionTile; import com.android.systemui.qs.tiles.DataSaverTile; @@ -54,7 +53,6 @@ import com.android.systemui.qs.tiles.ReduceBrightColorsTile; import com.android.systemui.qs.tiles.RotationLockTile; import com.android.systemui.qs.tiles.ScreenRecordTile; import com.android.systemui.qs.tiles.UiModeNightTile; -import com.android.systemui.qs.tiles.WifiTile; import com.android.systemui.qs.tiles.WorkModeTile; import com.android.systemui.util.leak.GarbageMonitor; @@ -68,10 +66,8 @@ public class QSFactoryImpl implements QSFactory { private static final String TAG = "QSFactory"; - private final Provider<WifiTile> mWifiTileProvider; private final Provider<InternetTile> mInternetTileProvider; private final Provider<BluetoothTile> mBluetoothTileProvider; - private final Provider<CellularTile> mCellularTileProvider; private final Provider<DndTile> mDndTileProvider; private final Provider<ColorCorrectionTile> mColorCorrectionTileProvider; private final Provider<ColorInversionTile> mColorInversionTileProvider; @@ -106,10 +102,8 @@ public class QSFactoryImpl implements QSFactory { public QSFactoryImpl( Lazy<QSHost> qsHostLazy, Provider<CustomTile.Builder> customTileBuilderProvider, - Provider<WifiTile> wifiTileProvider, Provider<InternetTile> internetTileProvider, Provider<BluetoothTile> bluetoothTileProvider, - Provider<CellularTile> cellularTileProvider, Provider<DndTile> dndTileProvider, Provider<ColorInversionTile> colorInversionTileProvider, Provider<AirplaneModeTile> airplaneModeTileProvider, @@ -139,10 +133,8 @@ public class QSFactoryImpl implements QSFactory { mQsHostLazy = qsHostLazy; mCustomTileBuilderProvider = customTileBuilderProvider; - mWifiTileProvider = wifiTileProvider; mInternetTileProvider = internetTileProvider; mBluetoothTileProvider = bluetoothTileProvider; - mCellularTileProvider = cellularTileProvider; mDndTileProvider = dndTileProvider; mColorInversionTileProvider = colorInversionTileProvider; mAirplaneModeTileProvider = airplaneModeTileProvider; @@ -186,14 +178,10 @@ public class QSFactoryImpl implements QSFactory { protected QSTileImpl createTileInternal(String tileSpec) { // Stock tiles. switch (tileSpec) { - case "wifi": - return mWifiTileProvider.get(); case "internet": return mInternetTileProvider.get(); case "bt": return mBluetoothTileProvider.get(); - case "cell": - return mCellularTileProvider.get(); case "dnd": return mDndTileProvider.get(); case "inversion": diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java deleted file mode 100644 index 04a25fc193b3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (C) 2014 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.qs.tiles; - -import static com.android.systemui.Prefs.Key.QS_HAS_TURNED_OFF_MOBILE_DATA; - -import android.annotation.NonNull; -import android.app.AlertDialog; -import android.app.AlertDialog.Builder; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.os.Handler; -import android.os.Looper; -import android.os.UserHandle; -import android.provider.Settings; -import android.service.quicksettings.Tile; -import android.telephony.SubscriptionManager; -import android.text.Html; -import android.text.TextUtils; -import android.view.View; -import android.view.WindowManager.LayoutParams; -import android.widget.Switch; - -import androidx.annotation.Nullable; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settingslib.net.DataUsageController; -import com.android.systemui.Prefs; -import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.qs.QSIconView; -import com.android.systemui.plugins.qs.QSTile.SignalState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.qs.QSHost; -import com.android.systemui.qs.SignalTileView; -import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.qs.tileimpl.QSTileImpl; -import com.android.systemui.statusbar.connectivity.IconState; -import com.android.systemui.statusbar.connectivity.MobileDataIndicators; -import com.android.systemui.statusbar.connectivity.NetworkController; -import com.android.systemui.statusbar.connectivity.SignalCallback; -import com.android.systemui.statusbar.phone.SystemUIDialog; -import com.android.systemui.statusbar.policy.KeyguardStateController; - -import javax.inject.Inject; - -/** Quick settings tile: Cellular **/ -public class CellularTile extends QSTileImpl<SignalState> { - private static final String ENABLE_SETTINGS_DATA_PLAN = "enable.settings.data.plan"; - - private final NetworkController mController; - private final DataUsageController mDataController; - private final KeyguardStateController mKeyguard; - private final CellSignalCallback mSignalCallback = new CellSignalCallback(); - - @Inject - public CellularTile( - QSHost host, - @Background Looper backgroundLooper, - @Main Handler mainHandler, - FalsingManager falsingManager, - MetricsLogger metricsLogger, - StatusBarStateController statusBarStateController, - ActivityStarter activityStarter, - QSLogger qsLogger, - NetworkController networkController, - KeyguardStateController keyguardStateController - - ) { - super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, - statusBarStateController, activityStarter, qsLogger); - mController = networkController; - mKeyguard = keyguardStateController; - mDataController = mController.getMobileDataController(); - mController.observe(getLifecycle(), mSignalCallback); - } - - @Override - public SignalState newTileState() { - return new SignalState(); - } - - @Override - public QSIconView createTileView(Context context) { - return new SignalTileView(context); - } - - @Override - public Intent getLongClickIntent() { - if (getState().state == Tile.STATE_UNAVAILABLE) { - return new Intent(Settings.ACTION_WIRELESS_SETTINGS); - } - return getCellularSettingIntent(); - } - - @Override - protected void handleClick(@Nullable View view) { - if (getState().state == Tile.STATE_UNAVAILABLE) { - return; - } - if (mDataController.isMobileDataEnabled()) { - maybeShowDisableDialog(); - } else { - mDataController.setMobileDataEnabled(true); - } - } - - private void maybeShowDisableDialog() { - if (Prefs.getBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, false)) { - // Directly turn off mobile data if the user has seen the dialog before. - mDataController.setMobileDataEnabled(false); - return; - } - String carrierName = mController.getMobileDataNetworkName(); - boolean isInService = mController.isMobileDataNetworkInService(); - if (TextUtils.isEmpty(carrierName) || !isInService) { - carrierName = mContext.getString(R.string.mobile_data_disable_message_default_carrier); - } - AlertDialog dialog = new Builder(mContext) - .setTitle(R.string.mobile_data_disable_title) - .setMessage(mContext.getString(R.string.mobile_data_disable_message, carrierName)) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton( - com.android.internal.R.string.alert_windows_notification_turn_off_action, - (d, w) -> { - mDataController.setMobileDataEnabled(false); - Prefs.putBoolean(mContext, QS_HAS_TURNED_OFF_MOBILE_DATA, true); - }) - .create(); - dialog.getWindow().setType(LayoutParams.TYPE_KEYGUARD_DIALOG); - SystemUIDialog.setShowForAllUsers(dialog, true); - SystemUIDialog.registerDismissListener(dialog); - SystemUIDialog.setWindowOnTop(dialog, mKeyguard.isShowing()); - dialog.show(); - } - - @Override - protected void handleSecondaryClick(@Nullable View view) { - handleLongClick(view); - } - - @Override - public CharSequence getTileLabel() { - return mContext.getString(R.string.quick_settings_cellular_detail_title); - } - - @Override - protected void handleUpdateState(SignalState state, Object arg) { - CallbackInfo cb = (CallbackInfo) arg; - if (cb == null) { - cb = mSignalCallback.mInfo; - } - - final Resources r = mContext.getResources(); - state.label = r.getString(R.string.mobile_data); - boolean mobileDataEnabled = mDataController.isMobileDataSupported() - && mDataController.isMobileDataEnabled(); - state.value = mobileDataEnabled; - state.activityIn = mobileDataEnabled && cb.activityIn; - state.activityOut = mobileDataEnabled && cb.activityOut; - state.expandedAccessibilityClassName = Switch.class.getName(); - if (cb.noSim) { - state.icon = ResourceIcon.get(R.drawable.ic_qs_no_sim); - } else { - state.icon = ResourceIcon.get(R.drawable.ic_swap_vert); - } - - if (cb.noSim) { - state.state = Tile.STATE_UNAVAILABLE; - state.secondaryLabel = r.getString(R.string.keyguard_missing_sim_message_short); - } else if (cb.airplaneModeEnabled) { - state.state = Tile.STATE_UNAVAILABLE; - state.secondaryLabel = r.getString(R.string.status_bar_airplane); - } else if (mobileDataEnabled) { - state.state = Tile.STATE_ACTIVE; - state.secondaryLabel = appendMobileDataType( - // Only show carrier name if there are more than 1 subscription - cb.multipleSubs ? cb.dataSubscriptionName : "", - getMobileDataContentName(cb)); - } else { - state.state = Tile.STATE_INACTIVE; - state.secondaryLabel = r.getString(R.string.cell_data_off); - } - - state.contentDescription = state.label; - if (state.state == Tile.STATE_INACTIVE) { - // This information is appended later by converting the Tile.STATE_INACTIVE state. - state.stateDescription = ""; - } else { - state.stateDescription = state.secondaryLabel; - } - } - - private CharSequence appendMobileDataType(CharSequence current, CharSequence dataType) { - if (TextUtils.isEmpty(dataType)) { - return Html.fromHtml(current.toString(), 0); - } - if (TextUtils.isEmpty(current)) { - return Html.fromHtml(dataType.toString(), 0); - } - String concat = mContext.getString(R.string.mobile_carrier_text_format, current, dataType); - return Html.fromHtml(concat, 0); - } - - private CharSequence getMobileDataContentName(CallbackInfo cb) { - if (cb.roaming && !TextUtils.isEmpty(cb.dataContentDescription)) { - String roaming = mContext.getString(R.string.data_connection_roaming); - String dataDescription = cb.dataContentDescription.toString(); - return mContext.getString(R.string.mobile_data_text_format, roaming, dataDescription); - } - if (cb.roaming) { - return mContext.getString(R.string.data_connection_roaming); - } - return cb.dataContentDescription; - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.QS_CELLULAR; - } - - @Override - public boolean isAvailable() { - return mController.hasMobileDataFeature() - && mHost.getUserContext().getUserId() == UserHandle.USER_SYSTEM; - } - - private static final class CallbackInfo { - boolean airplaneModeEnabled; - @Nullable - CharSequence dataSubscriptionName; - @Nullable - CharSequence dataContentDescription; - boolean activityIn; - boolean activityOut; - boolean noSim; - boolean roaming; - boolean multipleSubs; - } - - private final class CellSignalCallback implements SignalCallback { - private final CallbackInfo mInfo = new CallbackInfo(); - - @Override - public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) { - if (indicators.qsIcon == null) { - // Not data sim, don't display. - return; - } - mInfo.dataSubscriptionName = mController.getMobileDataNetworkName(); - mInfo.dataContentDescription = indicators.qsDescription != null - ? indicators.typeContentDescriptionHtml : null; - mInfo.activityIn = indicators.activityIn; - mInfo.activityOut = indicators.activityOut; - mInfo.roaming = indicators.roaming; - mInfo.multipleSubs = mController.getNumberSubscriptions() > 1; - refreshState(mInfo); - } - - @Override - public void setNoSims(boolean show, boolean simDetected) { - mInfo.noSim = show; - refreshState(mInfo); - } - - @Override - public void setIsAirplaneMode(@NonNull IconState icon) { - mInfo.airplaneModeEnabled = icon.visible; - refreshState(mInfo); - } - } - - static Intent getCellularSettingIntent() { - Intent intent = new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS); - int dataSub = SubscriptionManager.getDefaultDataSubscriptionId(); - if (dataSub != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - intent.putExtra(Settings.EXTRA_SUB_ID, - SubscriptionManager.getDefaultDataSubscriptionId()); - } - return intent; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java deleted file mode 100644 index b2be56cca51e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (C) 2014 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.qs.tiles; - -import android.annotation.NonNull; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; -import android.service.quicksettings.Tile; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.widget.Switch; - -import androidx.annotation.Nullable; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.qs.QSIconView; -import com.android.systemui.plugins.qs.QSTile; -import com.android.systemui.plugins.qs.QSTile.SignalState; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.qs.AlphaControlledSignalTileView; -import com.android.systemui.qs.QSHost; -import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.qs.tileimpl.QSIconViewImpl; -import com.android.systemui.qs.tileimpl.QSTileImpl; -import com.android.systemui.statusbar.connectivity.AccessPointController; -import com.android.systemui.statusbar.connectivity.NetworkController; -import com.android.systemui.statusbar.connectivity.SignalCallback; -import com.android.systemui.statusbar.connectivity.WifiIcons; -import com.android.systemui.statusbar.connectivity.WifiIndicators; - -import javax.inject.Inject; - -/** Quick settings tile: Wifi **/ -public class WifiTile extends QSTileImpl<SignalState> { - private static final Intent WIFI_SETTINGS = new Intent(Settings.ACTION_WIFI_SETTINGS); - - protected final NetworkController mController; - private final AccessPointController mWifiController; - private final QSTile.SignalState mStateBeforeClick = newTileState(); - - protected final WifiSignalCallback mSignalCallback = new WifiSignalCallback(); - private boolean mExpectDisabled; - - @Inject - public WifiTile( - QSHost host, - @Background Looper backgroundLooper, - @Main Handler mainHandler, - FalsingManager falsingManager, - MetricsLogger metricsLogger, - StatusBarStateController statusBarStateController, - ActivityStarter activityStarter, - QSLogger qsLogger, - NetworkController networkController, - AccessPointController accessPointController - ) { - super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger, - statusBarStateController, activityStarter, qsLogger); - mController = networkController; - mWifiController = accessPointController; - mController.observe(getLifecycle(), mSignalCallback); - mStateBeforeClick.spec = "wifi"; - } - - @Override - public SignalState newTileState() { - return new SignalState(); - } - - @Override - public QSIconView createTileView(Context context) { - return new AlphaControlledSignalTileView(context); - } - - @Override - public Intent getLongClickIntent() { - return WIFI_SETTINGS; - } - - @Override - protected void handleClick(@Nullable View view) { - // Secondary clicks are header clicks, just toggle. - mState.copyTo(mStateBeforeClick); - boolean wifiEnabled = mState.value; - // Immediately enter transient state when turning on wifi. - refreshState(wifiEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING); - mController.setWifiEnabled(!wifiEnabled); - mExpectDisabled = wifiEnabled; - if (mExpectDisabled) { - mHandler.postDelayed(() -> { - if (mExpectDisabled) { - mExpectDisabled = false; - refreshState(); - } - }, QSIconViewImpl.QS_ANIM_LENGTH); - } - } - - @Override - protected void handleSecondaryClick(@Nullable View view) { - if (!mWifiController.canConfigWifi()) { - mActivityStarter.postStartActivityDismissingKeyguard( - new Intent(Settings.ACTION_WIFI_SETTINGS), 0); - return; - } - if (!mState.value) { - mController.setWifiEnabled(true); - } - } - - @Override - public CharSequence getTileLabel() { - return mContext.getString(R.string.quick_settings_wifi_label); - } - - @Override - protected void handleUpdateState(SignalState state, Object arg) { - if (DEBUG) Log.d(TAG, "handleUpdateState arg=" + arg); - final CallbackInfo cb = mSignalCallback.mInfo; - if (mExpectDisabled) { - if (cb.enabled) { - return; // Ignore updates until disabled event occurs. - } else { - mExpectDisabled = false; - } - } - boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING; - boolean wifiConnected = cb.enabled && (cb.wifiSignalIconId > 0) - && (cb.ssid != null || cb.wifiSignalIconId != WifiIcons.QS_WIFI_NO_NETWORK); - boolean wifiNotConnected = (cb.ssid == null) - && (cb.wifiSignalIconId == WifiIcons.QS_WIFI_NO_NETWORK); - if (state.slash == null) { - state.slash = new SlashState(); - state.slash.rotation = 6; - } - state.slash.isSlashed = false; - boolean isTransient = transientEnabling || cb.isTransient; - state.secondaryLabel = getSecondaryLabel(isTransient, cb.statusLabel); - state.state = Tile.STATE_ACTIVE; - state.dualTarget = true; - state.value = transientEnabling || cb.enabled; - state.activityIn = cb.enabled && cb.activityIn; - state.activityOut = cb.enabled && cb.activityOut; - final StringBuffer minimalContentDescription = new StringBuffer(); - final StringBuffer minimalStateDescription = new StringBuffer(); - final Resources r = mContext.getResources(); - if (isTransient) { - state.icon = ResourceIcon.get( - com.android.internal.R.drawable.ic_signal_wifi_transient_animation); - state.label = r.getString(R.string.quick_settings_wifi_label); - } else if (!state.value) { - state.slash.isSlashed = true; - state.state = Tile.STATE_INACTIVE; - state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_DISABLED); - state.label = r.getString(R.string.quick_settings_wifi_label); - } else if (wifiConnected) { - state.icon = ResourceIcon.get(cb.wifiSignalIconId); - state.label = cb.ssid != null ? removeDoubleQuotes(cb.ssid) : getTileLabel(); - } else if (wifiNotConnected) { - state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_NO_NETWORK); - state.label = r.getString(R.string.quick_settings_wifi_label); - } else { - state.icon = ResourceIcon.get(WifiIcons.QS_WIFI_NO_NETWORK); - state.label = r.getString(R.string.quick_settings_wifi_label); - } - minimalContentDescription.append( - mContext.getString(R.string.quick_settings_wifi_label)).append(","); - if (state.value) { - if (wifiConnected) { - minimalStateDescription.append(cb.wifiSignalContentDescription); - minimalContentDescription.append(removeDoubleQuotes(cb.ssid)); - if (!TextUtils.isEmpty(state.secondaryLabel)) { - minimalContentDescription.append(",").append(state.secondaryLabel); - } - } - } - state.stateDescription = minimalStateDescription.toString(); - state.contentDescription = minimalContentDescription.toString(); - state.dualLabelContentDescription = r.getString( - R.string.accessibility_quick_settings_open_settings, getTileLabel()); - state.expandedAccessibilityClassName = Switch.class.getName(); - } - - private CharSequence getSecondaryLabel(boolean isTransient, String statusLabel) { - return isTransient - ? mContext.getString(R.string.quick_settings_wifi_secondary_label_transient) - : statusLabel; - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.QS_WIFI; - } - - @Override - public boolean isAvailable() { - return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI); - } - - @Nullable - private static String removeDoubleQuotes(String string) { - if (string == null) return null; - final int length = string.length(); - if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) { - return string.substring(1, length - 1); - } - return string; - } - - protected static final class CallbackInfo { - boolean enabled; - boolean connected; - int wifiSignalIconId; - @Nullable - String ssid; - boolean activityIn; - boolean activityOut; - @Nullable - String wifiSignalContentDescription; - boolean isTransient; - @Nullable - public String statusLabel; - - @Override - public String toString() { - return new StringBuilder("CallbackInfo[") - .append("enabled=").append(enabled) - .append(",connected=").append(connected) - .append(",wifiSignalIconId=").append(wifiSignalIconId) - .append(",ssid=").append(ssid) - .append(",activityIn=").append(activityIn) - .append(",activityOut=").append(activityOut) - .append(",wifiSignalContentDescription=").append(wifiSignalContentDescription) - .append(",isTransient=").append(isTransient) - .append(']').toString(); - } - } - - protected final class WifiSignalCallback implements SignalCallback { - final CallbackInfo mInfo = new CallbackInfo(); - - @Override - public void setWifiIndicators(@NonNull WifiIndicators indicators) { - if (DEBUG) Log.d(TAG, "onWifiSignalChanged enabled=" + indicators.enabled); - if (indicators.qsIcon == null) { - return; - } - mInfo.enabled = indicators.enabled; - mInfo.connected = indicators.qsIcon.visible; - mInfo.wifiSignalIconId = indicators.qsIcon.icon; - mInfo.ssid = indicators.description; - mInfo.activityIn = indicators.activityIn; - mInfo.activityOut = indicators.activityOut; - mInfo.wifiSignalContentDescription = indicators.qsIcon.contentDescription; - mInfo.isTransient = indicators.isTransient; - mInfo.statusLabel = indicators.statusLabel; - refreshState(); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java index a6c7781d891c..72c6bfe371ce 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WorkModeTile.java @@ -101,7 +101,6 @@ public class WorkModeTile extends QSTileImpl<BooleanState> implements @MainThread public void onManagedProfileRemoved() { mHost.removeTile(getTileSpec()); - mHost.unmarkTileAsAutoAdded(getTileSpec()); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 91ebf79344b6..b21a4857c736 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -687,8 +687,8 @@ public class ScreenshotController { } }); if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) { - mScreenshotView.badgeScreenshot( - mContext.getPackageManager().getUserBadgeForDensity(owner, 0)); + mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon( + mContext.getDrawable(R.drawable.overlay_badge_background), owner)); } mScreenshotView.setScreenshot(mScreenBitmap, screenInsets); if (DEBUG_WINDOW) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 899cdb74274f..200a7dc08bb3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -1090,7 +1090,7 @@ public class ScreenshotView extends FrameLayout implements mScreenshotBadge.setVisibility(View.GONE); mScreenshotBadge.setImageDrawable(null); mPendingSharedTransition = false; - mActionsContainerBackground.setVisibility(View.GONE); + mActionsContainerBackground.setVisibility(View.INVISIBLE); mActionsContainer.setVisibility(View.GONE); mDismissButton.setVisibility(View.GONE); mScrollingScrim.setVisibility(View.GONE); diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 28da38b701bc..61390c582fd6 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -112,7 +112,7 @@ class UserTrackerImpl internal constructor( // These get called when a managed profile goes in or out of quiet mode. addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) - + addAction(Intent.ACTION_MANAGED_PROFILE_ADDED) addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED) addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED) } @@ -129,6 +129,7 @@ class UserTrackerImpl internal constructor( Intent.ACTION_USER_INFO_CHANGED, Intent.ACTION_MANAGED_PROFILE_AVAILABLE, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE, + Intent.ACTION_MANAGED_PROFILE_ADDED, Intent.ACTION_MANAGED_PROFILE_REMOVED, Intent.ACTION_MANAGED_PROFILE_UNLOCKED -> { handleProfilesChanged() diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index ecaabce70f8a..10130b0a0882 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -138,11 +138,13 @@ import com.android.systemui.flags.Flags; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; +import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel; @@ -362,6 +364,7 @@ public final class NotificationPanelViewController implements Dumpable { private final FragmentListener mQsFragmentListener = new QsFragmentListener(); private final AccessibilityDelegate mAccessibilityDelegate = new ShadeAccessibilityDelegate(); private final NotificationGutsManager mGutsManager; + private final AlternateBouncerInteractor mAlternateBouncerInteractor; private long mDownTime; private boolean mTouchSlopExceededBeforeDown; @@ -690,6 +693,7 @@ public final class NotificationPanelViewController implements Dumpable { private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel; private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel; private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel; + private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel; private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel; private KeyguardTransitionInteractor mKeyguardTransitionInteractor; @@ -698,6 +702,7 @@ public final class NotificationPanelViewController implements Dumpable { private int mDreamingToLockscreenTransitionTranslationY; private int mOccludedToLockscreenTransitionTranslationY; private int mLockscreenToDreamingTransitionTranslationY; + private int mGoneToDreamingTransitionTranslationY; private int mLockscreenToOccludedTransitionTranslationY; private boolean mUnocclusionTransitionFlagEnabled = false; @@ -733,6 +738,12 @@ public final class NotificationPanelViewController implements Dumpable { step.getTransitionState() == TransitionState.RUNNING; }; + private final Consumer<TransitionStep> mGoneToDreamingTransition = + (TransitionStep step) -> { + mIsOcclusionTransitionRunning = + step.getTransitionState() == TransitionState.RUNNING; + }; + private final Consumer<TransitionStep> mLockscreenToOccludedTransition = (TransitionStep step) -> { mIsOcclusionTransitionRunning = @@ -807,9 +818,11 @@ public final class NotificationPanelViewController implements Dumpable { SystemClock systemClock, KeyguardBottomAreaViewModel keyguardBottomAreaViewModel, KeyguardBottomAreaInteractor keyguardBottomAreaInteractor, + AlternateBouncerInteractor alternateBouncerInteractor, DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel, OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel, LockscreenToDreamingTransitionViewModel lockscreenToDreamingTransitionViewModel, + GoneToDreamingTransitionViewModel goneToDreamingTransitionViewModel, LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel, @Main CoroutineDispatcher mainDispatcher, KeyguardTransitionInteractor keyguardTransitionInteractor, @@ -831,6 +844,7 @@ public final class NotificationPanelViewController implements Dumpable { mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel; mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel; mLockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel; + mGoneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel; mLockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel; mKeyguardTransitionInteractor = keyguardTransitionInteractor; mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @@ -1002,6 +1016,7 @@ public final class NotificationPanelViewController implements Dumpable { unlockAnimationStarted(playingCannedAnimation, isWakeAndUnlock, startDelay); } }); + mAlternateBouncerInteractor = alternateBouncerInteractor; dumpManager.registerDumpable(this); } @@ -1168,6 +1183,17 @@ public final class NotificationPanelViewController implements Dumpable { setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher); + // Gone->Dreaming + collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(), + mGoneToDreamingTransition, mMainDispatcher); + collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(), + setTransitionAlpha(mNotificationStackScrollLayoutController), + mMainDispatcher); + collectFlow(mView, mGoneToDreamingTransitionViewModel.lockscreenTranslationY( + mGoneToDreamingTransitionTranslationY), + setTransitionY(mNotificationStackScrollLayoutController), + mMainDispatcher); + // Lockscreen->Occluded collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(), mLockscreenToOccludedTransition, mMainDispatcher); @@ -1219,6 +1245,8 @@ public final class NotificationPanelViewController implements Dumpable { R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y); mLockscreenToDreamingTransitionTranslationY = mResources.getDimensionPixelSize( R.dimen.lockscreen_to_dreaming_transition_lockscreen_translation_y); + mGoneToDreamingTransitionTranslationY = mResources.getDimensionPixelSize( + R.dimen.gone_to_dreaming_transition_lockscreen_translation_y); mLockscreenToOccludedTransitionTranslationY = mResources.getDimensionPixelSize( R.dimen.lockscreen_to_occluded_transition_lockscreen_translation_y); } @@ -2336,7 +2364,7 @@ public final class NotificationPanelViewController implements Dumpable { // When false, down but not synthesized motion event. mLastEventSynthesizedDown = mExpectingSynthesizedDown; mLastDownEvents.insert( - mSystemClock.currentTimeMillis(), + event.getEventTime(), mDownX, mDownY, mQsTouchAboveFalsingThreshold, @@ -2469,7 +2497,7 @@ public final class NotificationPanelViewController implements Dumpable { mInitialTouchY = event.getY(); mInitialTouchX = event.getX(); } - if (!isFullyCollapsed()) { + if (!isFullyCollapsed() && !isShadeOrQsHeightAnimationRunning()) { handleQsDown(event); } // defer touches on QQS to shade while shade is collapsing. Added margin for error @@ -4950,7 +4978,7 @@ public final class NotificationPanelViewController implements Dumpable { mUpdateFlingVelocity = vel; } } else if (!mCentralSurfaces.isBouncerShowing() - && !mStatusBarKeyguardViewManager.isShowingAlternateBouncer() + && !mAlternateBouncerInteractor.isVisibleState() && !mKeyguardStateController.isKeyguardGoingAway()) { onEmptySpaceClick(); onTrackingStopped(true); @@ -5259,6 +5287,11 @@ public final class NotificationPanelViewController implements Dumpable { } } + /** Returns whether a shade or QS expansion animation is running */ + private boolean isShadeOrQsHeightAnimationRunning() { + return mHeightAnimator != null && !mHintAnimationRunning && !mIsSpringBackAnimation; + } + /** * Phase 2: Bounce down. */ @@ -6276,8 +6309,7 @@ public final class NotificationPanelViewController implements Dumpable { mCollapsedAndHeadsUpOnDown = isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp(); addMovement(event); - boolean regularHeightAnimationRunning = mHeightAnimator != null - && !mHintAnimationRunning && !mIsSpringBackAnimation; + boolean regularHeightAnimationRunning = isShadeOrQsHeightAnimationRunning(); if (!mGestureWaitForTouchSlop || regularHeightAnimationRunning) { mTouchSlopExceeded = regularHeightAnimationRunning || mTouchSlopExceededBeforeDown; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 8314ec713ccb..26f8b6222dc1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -321,9 +321,12 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway; if (onKeyguard && mAuthController.isUdfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser())) { + // both max and min display refresh rate must be set to take effect: mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardPreferredRefreshRate; + mLpChanged.preferredMinDisplayRefreshRate = mKeyguardPreferredRefreshRate; } else { mLpChanged.preferredMaxDisplayRefreshRate = 0; + mLpChanged.preferredMinDisplayRefreshRate = 0; } Trace.setCounter("display_set_preferred_refresh_rate", (long) mKeyguardPreferredRefreshRate); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 77307f3de19d..7ed6e3e55623 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -41,6 +41,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; @@ -85,6 +86,7 @@ public class NotificationShadeWindowViewController { private final AmbientState mAmbientState; private final PulsingGestureListener mPulsingGestureListener; private final NotificationInsetsController mNotificationInsetsController; + private final AlternateBouncerInteractor mAlternateBouncerInteractor; private GestureDetector mPulsingWakeupGestureHandler; private View mBrightnessMirror; @@ -132,8 +134,9 @@ public class NotificationShadeWindowViewController { PulsingGestureListener pulsingGestureListener, FeatureFlags featureFlags, KeyguardBouncerViewModel keyguardBouncerViewModel, - KeyguardTransitionInteractor keyguardTransitionInteractor, - KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory + KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory, + AlternateBouncerInteractor alternateBouncerInteractor, + KeyguardTransitionInteractor keyguardTransitionInteractor ) { mLockscreenShadeTransitionController = transitionController; mFalsingCollector = falsingCollector; @@ -153,6 +156,7 @@ public class NotificationShadeWindowViewController { mAmbientState = ambientState; mPulsingGestureListener = pulsingGestureListener; mNotificationInsetsController = notificationInsetsController; + mAlternateBouncerInteractor = alternateBouncerInteractor; // This view is not part of the newly inflated expanded status bar. mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container); @@ -315,7 +319,7 @@ public class NotificationShadeWindowViewController { return true; } - if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { // capture all touches if the alt auth bouncer is showing return true; } @@ -353,7 +357,7 @@ public class NotificationShadeWindowViewController { handled = !mService.isPulsing(); } - if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { // eat the touch handled = true; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index 5fedbeb556c2..11617be40f53 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -36,16 +36,9 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { buffer.log(TAG, LogLevel.DEBUG, msg) } - private inline fun log( - logLevel: LogLevel, - initializer: LogMessage.() -> Unit, - noinline printer: LogMessage.() -> String - ) { - buffer.log(TAG, logLevel, initializer, printer) - } - fun onQsInterceptMoveQsTrackingEnabled(h: Float) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { double1 = h.toDouble() }, { "onQsIntercept: move action, QS tracking enabled. h = $double1" } @@ -62,7 +55,8 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { keyguardShowing: Boolean, qsExpansionEnabled: Boolean ) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { int1 = initialTouchY.toInt() @@ -82,7 +76,8 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { } fun logMotionEvent(event: MotionEvent, message: String) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { str1 = message @@ -99,7 +94,8 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { } fun logMotionEventStatusBarState(event: MotionEvent, statusBarState: Int, message: String) { - log( + buffer.log( + TAG, LogLevel.VERBOSE, { str1 = message @@ -128,25 +124,33 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { tracking: Boolean, dragDownPxAmount: Float, ) { - log(LogLevel.VERBOSE, { - str1 = message - double1 = fraction.toDouble() - bool1 = expanded - bool2 = tracking - long1 = dragDownPxAmount.toLong() - }, { - "$str1 fraction=$double1,expanded=$bool1," + + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = message + double1 = fraction.toDouble() + bool1 = expanded + bool2 = tracking + long1 = dragDownPxAmount.toLong() + }, + { + "$str1 fraction=$double1,expanded=$bool1," + "tracking=$bool2," + "dragDownPxAmount=$dragDownPxAmount" - }) + } + ) } fun logHasVibrated(hasVibratedOnOpen: Boolean, fraction: Float) { - log(LogLevel.VERBOSE, { - bool1 = hasVibratedOnOpen - double1 = fraction.toDouble() - }, { - "hasVibratedOnOpen=$bool1, expansionFraction=$double1" - }) + buffer.log( + TAG, + LogLevel.VERBOSE, + { + bool1 = hasVibratedOnOpen + double1 = fraction.toDouble() + }, + { "hasVibratedOnOpen=$bool1, expansionFraction=$double1" } + ) } fun logQsExpansionChanged( @@ -159,42 +163,56 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { qsAnimatorExpand: Boolean, animatingQs: Boolean ) { - log(LogLevel.VERBOSE, { - str1 = message - bool1 = qsExpanded - int1 = qsMinExpansionHeight - int2 = qsMaxExpansionHeight - bool2 = stackScrollerOverscrolling - bool3 = dozing - bool4 = qsAnimatorExpand - // 0 = false, 1 = true - long1 = animatingQs.compareTo(false).toLong() - }, { - "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," + + buffer.log( + TAG, + LogLevel.VERBOSE, + { + str1 = message + bool1 = qsExpanded + int1 = qsMinExpansionHeight + int2 = qsMaxExpansionHeight + bool2 = stackScrollerOverscrolling + bool3 = dozing + bool4 = qsAnimatorExpand + // 0 = false, 1 = true + long1 = animatingQs.compareTo(false).toLong() + }, + { + "$str1 qsExpanded=$bool1,qsMinExpansionHeight=$int1,qsMaxExpansionHeight=$int2," + "stackScrollerOverscrolling=$bool2,dozing=$bool3,qsAnimatorExpand=$bool4," + "animatingQs=$long1" - }) + } + ) } fun logSingleTapUp(isDozing: Boolean, singleTapEnabled: Boolean, isNotDocked: Boolean) { - log(LogLevel.DEBUG, { - bool1 = isDozing - bool2 = singleTapEnabled - bool3 = isNotDocked - }, { - "PulsingGestureListener#onSingleTapUp all of this must true for single " + - "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3" + buffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = isDozing + bool2 = singleTapEnabled + bool3 = isNotDocked + }, + { + "PulsingGestureListener#onSingleTapUp all of this must true for single " + + "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3" }) } fun logSingleTapUpFalsingState(proximityIsNotNear: Boolean, isNotFalseTap: Boolean) { - log(LogLevel.DEBUG, { - bool1 = proximityIsNotNear - bool2 = isNotFalseTap - }, { - "PulsingGestureListener#onSingleTapUp all of this must true for single " + + buffer.log( + TAG, + LogLevel.DEBUG, + { + bool1 = proximityIsNotNear + bool2 = isNotFalseTap + }, + { + "PulsingGestureListener#onSingleTapUp all of this must true for single " + "tap to be detected: proximityIsNotNear: $bool1, isNotFalseTap: $bool2" - }) + } + ) } fun logNotInterceptingTouchInstantExpanding( @@ -202,13 +220,18 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { notificationsDragEnabled: Boolean, touchDisabled: Boolean ) { - log(LogLevel.VERBOSE, { - bool1 = instantExpanding - bool2 = notificationsDragEnabled - bool3 = touchDisabled - }, { - "NPVC not intercepting touch, instantExpanding: $bool1, " + + buffer.log( + TAG, + LogLevel.VERBOSE, + { + bool1 = instantExpanding + bool2 = notificationsDragEnabled + bool3 = touchDisabled + }, + { + "NPVC not intercepting touch, instantExpanding: $bool1, " + "!notificationsDragEnabled: $bool2, touchDisabled: $bool3" - }) + } + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt index c6a6e875b82d..9851625b6152 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeWindowLogger.kt @@ -32,11 +32,21 @@ class ShadeWindowLogger @Inject constructor(@ShadeWindowLog private val buffer: ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) { fun logApplyingWindowLayoutParams(lp: WindowManager.LayoutParams) { - log(DEBUG, { str1 = lp.toString() }, { "Applying new window layout params: $str1" }) + buffer.log( + TAG, + DEBUG, + { str1 = lp.toString() }, + { "Applying new window layout params: $str1" } + ) } fun logNewState(state: Any) { - log(DEBUG, { str1 = state.toString() }, { "Applying new state: $str1" }) + buffer.log( + TAG, + DEBUG, + { str1 = state.toString() }, + { "Applying new state: $str1" } + ) } private inline fun log( @@ -48,11 +58,16 @@ class ShadeWindowLogger @Inject constructor(@ShadeWindowLog private val buffer: } fun logApplyVisibility(visible: Boolean) { - log(DEBUG, { bool1 = visible }, { "Updating visibility, should be visible : $bool1" }) + buffer.log( + TAG, + DEBUG, + { bool1 = visible }, + { "Updating visibility, should be visible : $bool1" }) } fun logShadeVisibleAndFocusable(visible: Boolean) { - log( + buffer.log( + TAG, DEBUG, { bool1 = visible }, { "Updating shade, should be visible and focusable: $bool1" } @@ -60,6 +75,11 @@ class ShadeWindowLogger @Inject constructor(@ShadeWindowLog private val buffer: } fun logShadeFocusable(focusable: Boolean) { - log(DEBUG, { bool1 = focusable }, { "Updating shade, should be focusable : $bool1" }) + buffer.log( + TAG, + DEBUG, + { bool1 = focusable }, + { "Updating shade, should be focusable : $bool1" } + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 750d00466a8d..584a382184f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -334,7 +334,7 @@ public class CommandQueue extends IStatusBar.Stub implements /** * @see IStatusBar#setBiometicContextListener(IBiometricContextListener) */ - default void setBiometicContextListener(IBiometricContextListener listener) { + default void setBiometricContextListener(IBiometricContextListener listener) { } /** @@ -1583,7 +1583,7 @@ public class CommandQueue extends IStatusBar.Stub implements } case MSG_SET_BIOMETRICS_LISTENER: for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).setBiometicContextListener( + mCallbacks.get(i).setBiometricContextListener( (IBiometricContextListener) msg.obj); } break; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 37d26c29d6f0..006b5528e7e9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -91,6 +91,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardIndication; import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -156,6 +157,7 @@ public class KeyguardIndicationController { private final KeyguardBypassController mKeyguardBypassController; private final AccessibilityManager mAccessibilityManager; private final Handler mHandler; + private final AlternateBouncerInteractor mAlternateBouncerInteractor; @VisibleForTesting public KeyguardIndicationRotateTextViewController mRotateTextViewController; @@ -235,7 +237,8 @@ public class KeyguardIndicationController { KeyguardBypassController keyguardBypassController, AccessibilityManager accessibilityManager, FaceHelpMessageDeferral faceHelpMessageDeferral, - KeyguardLogger keyguardLogger) { + KeyguardLogger keyguardLogger, + AlternateBouncerInteractor alternateBouncerInteractor) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; mDevicePolicyManager = devicePolicyManager; @@ -257,6 +260,7 @@ public class KeyguardIndicationController { mScreenLifecycle = screenLifecycle; mKeyguardLogger = keyguardLogger; mScreenLifecycle.addObserver(mScreenObserver); + mAlternateBouncerInteractor = alternateBouncerInteractor; mFaceAcquiredMessageDeferral = faceHelpMessageDeferral; mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>(); @@ -929,7 +933,7 @@ public class KeyguardIndicationController { } if (mStatusBarKeyguardViewManager.isBouncerShowing()) { - if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { return; // udfps affordance is highlighted, no need to show action to unlock } else if (mKeyguardUpdateMonitor.isFaceEnrolled() && !mKeyguardUpdateMonitor.getIsFaceAuthenticated()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 8f9365cd4dc4..99081e98c4a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -65,8 +65,6 @@ import com.android.systemui.statusbar.policy.RemoteInputView; import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.ListenerSet; -import dagger.Lazy; - import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -74,6 +72,8 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; +import dagger.Lazy; + /** * Class for handling remote input state over a set of notifications. This class handles things * like keeping notifications temporarily that were cancelled as a response to a remote input @@ -465,8 +465,7 @@ public class NotificationRemoteInputManager implements Dumpable { riv.getController().setRemoteInput(input); riv.getController().setRemoteInputs(inputs); riv.getController().setEditedSuggestionInfo(editedSuggestionInfo); - ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null; - riv.focusAnimated(parent); + riv.focusAnimated(); if (userMessageContent != null) { riv.setEditTextContent(userMessageContent); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 14d0d7e032d5..9a65e342478e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -31,6 +31,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpHandler; import com.android.systemui.dump.DumpManager; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -61,7 +62,6 @@ import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl; import com.android.systemui.statusbar.phone.StatusBarIconList; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController; import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallFlags; @@ -280,7 +280,7 @@ public interface CentralSurfacesDependenciesModule { @SysUISingleton static DialogLaunchAnimator provideDialogLaunchAnimator(IDreamManager dreamManager, KeyguardStateController keyguardStateController, - Lazy<StatusBarKeyguardViewManager> statusBarKeyguardViewManager, + Lazy<AlternateBouncerInteractor> alternateBouncerInteractor, InteractionJankMonitor interactionJankMonitor) { DialogLaunchAnimator.Callback callback = new DialogLaunchAnimator.Callback() { @Override @@ -300,7 +300,7 @@ public interface CentralSurfacesDependenciesModule { @Override public boolean isShowingAlternateAuthOnUnlock() { - return statusBarKeyguardViewManager.get().canShowAlternateBouncer(); + return alternateBouncerInteractor.get().canShowAlternateBouncerForFingerprint(); } }; return new DialogLaunchAnimator(callback, interactionJankMonitor); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java index c4961029dc33..b084a765956d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java @@ -109,7 +109,7 @@ public class ActivatableNotificationViewController return true; } if (ev.getAction() == MotionEvent.ACTION_UP) { - mView.setLastActionUpTime(SystemClock.uptimeMillis()); + mView.setLastActionUpTime(ev.getEventTime()); } // With a11y, just do nothing. if (mAccessibilityManager.isTouchExplorationEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 8d48d738f0f2..9b93d7b9e1d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -1431,6 +1431,22 @@ public class NotificationChildrenContainer extends ViewGroup @Override public void applyRoundnessAndInvalidate() { boolean last = true; + if (mUseRoundnessSourceTypes) { + if (mNotificationHeaderWrapper != null) { + mNotificationHeaderWrapper.requestTopRoundness( + /* value = */ getTopRoundness(), + /* sourceType = */ FROM_PARENT, + /* animate = */ false + ); + } + if (mNotificationHeaderWrapperLowPriority != null) { + mNotificationHeaderWrapperLowPriority.requestTopRoundness( + /* value = */ getTopRoundness(), + /* sourceType = */ FROM_PARENT, + /* animate = */ false + ); + } + } for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == View.GONE) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index ca1e397f930a..356ddfa2d482 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -1811,9 +1811,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override @ShadeViewRefactor(RefactorComponent.COORDINATOR) public WindowInsets onApplyWindowInsets(WindowInsets insets) { - mBottomInset = insets.getSystemWindowInsetBottom() - + insets.getInsets(WindowInsets.Type.ime()).bottom; - + mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom; mWaterfallTopInset = 0; final DisplayCutout cutout = insets.getDisplayCutout(); if (cutout != null) { @@ -2262,7 +2260,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.COORDINATOR) private int getImeInset() { - return Math.max(0, mBottomInset - (getRootView().getHeight() - getHeight())); + // The NotificationStackScrollLayout does not extend all the way to the bottom of the + // display. Therefore, subtract that space from the mBottomInset, in order to only include + // the portion of the bottom inset that actually overlaps the NotificationStackScrollLayout. + return Math.max(0, mBottomInset + - (getRootView().getHeight() - getHeight() - getLocationOnScreen()[1])); } /** @@ -2970,12 +2972,19 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable childInGroup = (ExpandableNotificationRow) requestedView; requestedView = requestedRow = childInGroup.getNotificationParent(); } - int position = 0; + final float scrimTopPadding = mAmbientState.isOnKeyguard() ? 0 : mMinimumPaddings; + int position = (int) scrimTopPadding; + int visibleIndex = -1; + ExpandableView lastVisibleChild = null; for (int i = 0; i < getChildCount(); i++) { ExpandableView child = getChildAtIndex(i); boolean notGone = child.getVisibility() != View.GONE; + if (notGone) visibleIndex++; if (notGone && !child.hasNoContentHeight()) { - if (position != 0) { + if (position != scrimTopPadding) { + if (lastVisibleChild != null) { + position += calculateGapHeight(lastVisibleChild, child, visibleIndex); + } position += mPaddingBetweenElements; } } @@ -2987,6 +2996,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } if (notGone) { position += getIntrinsicHeight(child); + lastVisibleChild = child; } } return 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java index 9070eadd9944..149ec545dfa7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java @@ -154,9 +154,7 @@ public class AutoTileManager implements UserAwareController { if (!mAutoTracker.isAdded(SAVER)) { mDataSaverController.addCallback(mDataSaverListener); } - if (!mAutoTracker.isAdded(WORK)) { - mManagedProfileController.addCallback(mProfileCallback); - } + mManagedProfileController.addCallback(mProfileCallback); if (!mAutoTracker.isAdded(NIGHT) && ColorDisplayManager.isNightDisplayAvailable(mContext)) { mNightDisplayListener.setCallback(mNightDisplayCallback); @@ -275,18 +273,18 @@ public class AutoTileManager implements UserAwareController { return mCurrentUser.getIdentifier(); } - public void unmarkTileAsAutoAdded(String tabSpec) { - mAutoTracker.setTileRemoved(tabSpec); - } - private final ManagedProfileController.Callback mProfileCallback = new ManagedProfileController.Callback() { @Override public void onManagedProfileChanged() { - if (mAutoTracker.isAdded(WORK)) return; if (mManagedProfileController.hasActiveProfile()) { + if (mAutoTracker.isAdded(WORK)) return; mHost.addTile(WORK); mAutoTracker.setTileAdded(WORK); + } else { + if (!mAutoTracker.isAdded(WORK)) return; + mHost.removeTile(WORK); + mAutoTracker.setTileRemoved(WORK); } } @@ -429,7 +427,7 @@ public class AutoTileManager implements UserAwareController { initSafetyTile(); } else if (!isSafetyCenterEnabled && mAutoTracker.isAdded(mSafetySpec)) { mHost.removeTile(mSafetySpec); - mHost.unmarkTileAsAutoAdded(mSafetySpec); + mAutoTracker.setTileRemoved(mSafetySpec); } } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 895a2934ec1b..db2c0a08c1d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.phone; import static android.app.StatusBarManager.SESSION_KEYGUARD; +import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME; + import android.annotation.IntDef; import android.content.res.Resources; import android.hardware.biometrics.BiometricFaceConstants; @@ -27,7 +29,6 @@ import android.hardware.fingerprint.FingerprintManager; import android.metrics.LogMaker; import android.os.Handler; import android.os.PowerManager; -import android.os.SystemClock; import android.os.Trace; import androidx.annotation.Nullable; @@ -62,6 +63,7 @@ import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.time.SystemClock; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -78,6 +80,7 @@ import javax.inject.Inject; */ @SysUISingleton public class BiometricUnlockController extends KeyguardUpdateMonitorCallback implements Dumpable { + private static final long RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS = 400L; private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000; private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); @@ -169,9 +172,11 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private final MetricsLogger mMetricsLogger; private final AuthController mAuthController; private final StatusBarStateController mStatusBarStateController; + private final WakefulnessLifecycle mWakefulnessLifecycle; private final LatencyTracker mLatencyTracker; private final VibratorHelper mVibratorHelper; private final BiometricUnlockLogger mLogger; + private final SystemClock mSystemClock; private long mLastFpFailureUptimeMillis; private int mNumConsecutiveFpFailures; @@ -279,14 +284,17 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp SessionTracker sessionTracker, LatencyTracker latencyTracker, ScreenOffAnimationController screenOffAnimationController, - VibratorHelper vibrator) { + VibratorHelper vibrator, + SystemClock systemClock + ) { mPowerManager = powerManager; mShadeController = shadeController; mUpdateMonitor = keyguardUpdateMonitor; mUpdateMonitor.registerCallback(this); mMediaManager = notificationMediaManager; mLatencyTracker = latencyTracker; - wakefulnessLifecycle.addObserver(mWakefulnessObserver); + mWakefulnessLifecycle = wakefulnessLifecycle; + mWakefulnessLifecycle.addObserver(mWakefulnessObserver); screenLifecycle.addObserver(mScreenObserver); mNotificationShadeWindowController = notificationShadeWindowController; @@ -306,6 +314,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mScreenOffAnimationController = screenOffAnimationController; mVibratorHelper = vibrator; mLogger = biometricUnlockLogger; + mSystemClock = systemClock; dumpManager.registerDumpable(getClass().getName(), this); } @@ -429,8 +438,11 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp Runnable wakeUp = ()-> { if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) { mLogger.i("bio wakelock: Authenticated, waking up..."); - mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC, - "android.policy:BIOMETRIC"); + mPowerManager.wakeUp( + mSystemClock.uptimeMillis(), + PowerManager.WAKE_REASON_BIOMETRIC, + "android.policy:BIOMETRIC" + ); } Trace.beginSection("release wake-and-unlock"); releaseBiometricWakeLock(); @@ -670,7 +682,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp startWakeAndUnlock(MODE_ONLY_WAKE); } else if (biometricSourceType == BiometricSourceType.FINGERPRINT && mUpdateMonitor.isUdfpsSupported()) { - long currUptimeMillis = SystemClock.uptimeMillis(); + long currUptimeMillis = mSystemClock.uptimeMillis(); if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) { mNumConsecutiveFpFailures += 1; } else { @@ -718,12 +730,26 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp cleanup(); } - //these haptics are for device-entry only + // these haptics are for device-entry only private void vibrateSuccess(BiometricSourceType type) { + if (mAuthController.isSfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()) + && lastWakeupFromPowerButtonWithinHapticThreshold()) { + mLogger.d("Skip auth success haptic. Power button was recently pressed."); + return; + } mVibratorHelper.vibrateAuthSuccess( getClass().getSimpleName() + ", type =" + type + "device-entry::success"); } + private boolean lastWakeupFromPowerButtonWithinHapticThreshold() { + final boolean lastWakeupFromPowerButton = mWakefulnessLifecycle.getLastWakeReason() + == PowerManager.WAKE_REASON_POWER_BUTTON; + return lastWakeupFromPowerButton + && mWakefulnessLifecycle.getLastWakeTime() != UNKNOWN_LAST_WAKE_TIME + && mSystemClock.uptimeMillis() - mWakefulnessLifecycle.getLastWakeTime() + < RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS; + } + private void vibrateError(BiometricSourceType type) { mVibratorHelper.vibrateAuthError( getClass().getSimpleName() + ", type =" + type + "device-entry::error"); @@ -816,7 +842,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp if (mUpdateMonitor.isUdfpsSupported()) { pw.print(" mNumConsecutiveFpFailures="); pw.println(mNumConsecutiveFpFailures); pw.print(" time since last failure="); - pw.println(SystemClock.uptimeMillis() - mLastFpFailureUptimeMillis); + pw.println(mSystemClock.uptimeMillis() - mLastFpFailureUptimeMillis); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 198572a51760..f1e1f426b467 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -162,6 +162,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder; import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel; import com.android.systemui.navigationbar.NavigationBarController; @@ -485,6 +486,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final ShadeController mShadeController; private final InitController mInitController; private final Lazy<CameraLauncher> mCameraLauncherLazy; + private final AlternateBouncerInteractor mAlternateBouncerInteractor; private final PluginDependencyProvider mPluginDependencyProvider; private final KeyguardDismissUtil mKeyguardDismissUtil; @@ -763,7 +765,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { WiredChargingRippleController wiredChargingRippleController, IDreamManager dreamManager, Lazy<CameraLauncher> cameraLauncherLazy, - Lazy<LightRevealScrimViewModel> lightRevealScrimViewModelLazy) { + Lazy<LightRevealScrimViewModel> lightRevealScrimViewModelLazy, + AlternateBouncerInteractor alternateBouncerInteractor + ) { mContext = context; mNotificationsController = notificationsController; mFragmentService = fragmentService; @@ -841,6 +845,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mWallpaperManager = wallpaperManager; mJankMonitor = jankMonitor; mCameraLauncherLazy = cameraLauncherLazy; + mAlternateBouncerInteractor = alternateBouncerInteractor; mLockscreenShadeTransitionController = lockscreenShadeTransitionController; mStartingSurfaceOptional = startingSurfaceOptional; @@ -3257,8 +3262,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private void showBouncerOrLockScreenIfKeyguard() { // If the keyguard is animating away, we aren't really the keyguard anymore and should not // show the bouncer/lockscreen. - if (!mKeyguardViewMediator.isHiding() - && !mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) { + if (!mKeyguardViewMediator.isHiding() && !mKeyguardUpdateMonitor.isKeyguardGoingAway()) { if (mState == StatusBarState.SHADE_LOCKED) { // shade is showing while locked on the keyguard, so go back to showing the // lock screen where users can use the UDFPS affordance to enter the device @@ -3737,7 +3741,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { boolean launchingAffordanceWithPreview = mLaunchingAffordance; mScrimController.setLaunchingAffordanceWithPreview(launchingAffordanceWithPreview); - if (mStatusBarKeyguardViewManager.isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { if (mState == StatusBarState.SHADE || mState == StatusBarState.SHADE_LOCKED || mTransitionToFullShadeProgress > 0f) { mScrimController.transitionTo(ScrimState.AUTH_SCRIMMED_SHADE); @@ -4262,8 +4266,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void onDozeAmountChanged(float linear, float eased) { - if (mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) - && !mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION) + if (!mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION) && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) { mLightRevealScrim.setRevealAmount(1f - linear); } 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 de7b152adaab..0446cefb10dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -44,10 +44,9 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.tuner.TunerService; @@ -82,7 +81,6 @@ public class DozeParameters implements private final AlwaysOnDisplayPolicy mAlwaysOnPolicy; private final Resources mResources; private final BatteryController mBatteryController; - private final FeatureFlags mFeatureFlags; private final ScreenOffAnimationController mScreenOffAnimationController; private final FoldAodAnimationController mFoldAodAnimationController; private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; @@ -125,7 +123,6 @@ public class DozeParameters implements BatteryController batteryController, TunerService tunerService, DumpManager dumpManager, - FeatureFlags featureFlags, ScreenOffAnimationController screenOffAnimationController, Optional<SysUIUnfoldComponent> sysUiUnfoldComponent, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, @@ -141,7 +138,6 @@ public class DozeParameters implements mControlScreenOffAnimation = !getDisplayNeedsBlanking(); mPowerManager = powerManager; mPowerManager.setDozeAfterScreenOff(!mControlScreenOffAnimation); - mFeatureFlags = featureFlags; mScreenOffAnimationController = screenOffAnimationController; mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; @@ -162,6 +158,13 @@ public class DozeParameters implements SettingsObserver quickPickupSettingsObserver = new SettingsObserver(context, handler); quickPickupSettingsObserver.observe(); + + batteryController.addCallback(new BatteryStateChangeCallback() { + @Override + public void onPowerSaveChanged(boolean isPowerSave) { + dispatchAlwaysOnEvent(); + } + }); } private void updateQuickPickupEnabled() { @@ -300,13 +303,10 @@ public class DozeParameters implements /** * 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. + * possible if AOD isn't even enabled or if the display needs blanking. */ public boolean canControlUnlockedScreenOff() { - return getAlwaysOn() - && mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) - && !getDisplayNeedsBlanking(); + return getAlwaysOn() && !getDisplayNeedsBlanking(); } /** @@ -424,9 +424,7 @@ public class DozeParameters implements updateControlScreenOff(); } - for (Callback callback : mCallbacks) { - callback.onAlwaysOnChange(); - } + dispatchAlwaysOnEvent(); mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn()); } @@ -463,6 +461,12 @@ public class DozeParameters implements pw.print("isQuickPickupEnabled(): "); pw.println(isQuickPickupEnabled()); } + private void dispatchAlwaysOnEvent() { + for (Callback callback : mCallbacks) { + callback.onAlwaysOnChange(); + } + } + private boolean getPostureSpecificBool( int[] postureMapping, boolean defaultSensorBool, @@ -477,7 +481,8 @@ public class DozeParameters implements return bool; } - interface Callback { + /** Callbacks for doze parameter related information */ + public interface Callback { /** * Invoked when the value of getAlwaysOn may have changed. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index d480fabe75b1..7d917bd9cd1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -58,6 +58,7 @@ import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.data.BouncerView; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.navigationbar.NavigationBarView; @@ -134,6 +135,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController; private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; + private final AlternateBouncerInteractor mAlternateBouncerInteractor; private final BouncerView mPrimaryBouncerView; private final Lazy<ShadeController> mShadeController; @@ -253,6 +255,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>(); private boolean mIsModernBouncerEnabled; private boolean mIsUnoccludeTransitionFlagEnabled; + private boolean mIsModernAlternateBouncerEnabled; private OnDismissAction mAfterKeyguardGoneAction; private Runnable mKeyguardGoneCancelAction; @@ -269,7 +272,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private final LatencyTracker mLatencyTracker; private final KeyguardSecurityModel mKeyguardSecurityModel; @Nullable private KeyguardBypassController mBypassController; - @Nullable private AlternateBouncer mAlternateBouncer; + @Nullable private OccludingAppBiometricUI mOccludingAppBiometricUI; private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { @@ -306,7 +309,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb FeatureFlags featureFlags, PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor, PrimaryBouncerInteractor primaryBouncerInteractor, - BouncerView primaryBouncerView) { + BouncerView primaryBouncerView, + AlternateBouncerInteractor alternateBouncerInteractor) { mContext = context; mViewMediatorCallback = callback; mLockPatternUtils = lockPatternUtils; @@ -331,6 +335,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null); mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER); mIsUnoccludeTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION); + mIsModernAlternateBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER); + mAlternateBouncerInteractor = alternateBouncerInteractor; } @Override @@ -363,23 +369,51 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } /** - * Sets the given alt auth interceptor to null if it's the current auth interceptor. Else, + * Sets the given legacy alternate bouncer to null if it's the current alternate bouncer. Else, + * does nothing. Only used if modern alternate bouncer is NOT enabled. + */ + public void removeLegacyAlternateBouncer( + @NonNull LegacyAlternateBouncer alternateBouncerLegacy) { + if (!mIsModernAlternateBouncerEnabled) { + if (Objects.equals(mAlternateBouncerInteractor.getLegacyAlternateBouncer(), + alternateBouncerLegacy)) { + mAlternateBouncerInteractor.setLegacyAlternateBouncer(null); + hideAlternateBouncer(true); + } + } + } + + /** + * Sets a new legacy alternate bouncer. Only used if mdoern alternate bouncer is NOT enable. + */ + public void setLegacyAlternateBouncer(@NonNull LegacyAlternateBouncer alternateBouncerLegacy) { + if (!mIsModernAlternateBouncerEnabled) { + if (!Objects.equals(mAlternateBouncerInteractor.getLegacyAlternateBouncer(), + alternateBouncerLegacy)) { + mAlternateBouncerInteractor.setLegacyAlternateBouncer(alternateBouncerLegacy); + hideAlternateBouncer(false); + } + } + + } + + + /** + * Sets the given OccludingAppBiometricUI to null if it's the current auth interceptor. Else, * does nothing. */ - public void removeAlternateAuthInterceptor(@NonNull AlternateBouncer authInterceptor) { - if (Objects.equals(mAlternateBouncer, authInterceptor)) { - mAlternateBouncer = null; - hideAlternateBouncer(true); + public void removeOccludingAppBiometricUI(@NonNull OccludingAppBiometricUI biometricUI) { + if (Objects.equals(mOccludingAppBiometricUI, biometricUI)) { + mOccludingAppBiometricUI = null; } } /** - * Sets a new alt auth interceptor. + * Sets a new OccludingAppBiometricUI. */ - public void setAlternateBouncer(@NonNull AlternateBouncer authInterceptor) { - if (!Objects.equals(mAlternateBouncer, authInterceptor)) { - mAlternateBouncer = authInterceptor; - hideAlternateBouncer(false); + public void setOccludingAppBiometricUI(@NonNull OccludingAppBiometricUI biometricUI) { + if (!Objects.equals(mOccludingAppBiometricUI, biometricUI)) { + mOccludingAppBiometricUI = biometricUI; } } @@ -566,18 +600,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * {@see KeyguardBouncer#show(boolean, boolean)} */ public void showBouncer(boolean scrimmed) { - if (canShowAlternateBouncer()) { - updateAlternateBouncerShowing(mAlternateBouncer.showAlternateBouncer()); - return; + if (!mAlternateBouncerInteractor.show()) { + showPrimaryBouncer(scrimmed); + } else { + updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState()); } - - showPrimaryBouncer(scrimmed); - } - - /** Whether we can show the alternate bouncer instead of the primary bouncer. */ - public boolean canShowAlternateBouncer() { - return mAlternateBouncer != null - && mKeyguardUpdateManager.isUnlockingWithBiometricAllowed(true); } /** @@ -641,9 +668,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardGoneCancelAction = cancelAction; mDismissActionWillAnimateOnKeyguard = r != null && r.willRunAnimationOnKeyguard(); - // If there is an an alternate auth interceptor (like the UDFPS), show that one + // If there is an alternate auth interceptor (like the UDFPS), show that one // instead of the bouncer. - if (canShowAlternateBouncer()) { + if (mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()) { if (!afterKeyguardGone) { if (mPrimaryBouncer != null) { mPrimaryBouncer.setDismissAction(mAfterKeyguardGoneAction, @@ -656,7 +683,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardGoneCancelAction = null; } - updateAlternateBouncerShowing(mAlternateBouncer.showAlternateBouncer()); + updateAlternateBouncerShowing(mAlternateBouncerInteractor.show()); return; } @@ -725,10 +752,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void hideAlternateBouncer(boolean forceUpdateScrim) { - final boolean updateScrim = (mAlternateBouncer != null - && mAlternateBouncer.hideAlternateBouncer()) - || forceUpdateScrim; - updateAlternateBouncerShowing(updateScrim); + updateAlternateBouncerShowing(mAlternateBouncerInteractor.hide() || forceUpdateScrim); } private void updateAlternateBouncerShowing(boolean updateScrim) { @@ -738,7 +762,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb return; } - final boolean isShowingAlternateBouncer = isShowingAlternateBouncer(); + final boolean isShowingAlternateBouncer = mAlternateBouncerInteractor.isVisibleState(); if (mKeyguardMessageAreaController != null) { mKeyguardMessageAreaController.setIsVisible(isShowingAlternateBouncer); mKeyguardMessageAreaController.setMessage(""); @@ -1095,7 +1119,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public boolean isBouncerShowing() { - return primaryBouncerIsShowing() || isShowingAlternateBouncer(); + return primaryBouncerIsShowing() || mAlternateBouncerInteractor.isVisibleState(); } @Override @@ -1339,7 +1363,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mPrimaryBouncerInteractor.notifyKeyguardAuthenticated(strongAuth); } - if (mAlternateBouncer != null && isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { hideAlternateBouncer(false); executeAfterKeyguardGoneAction(); } @@ -1347,7 +1371,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb /** Display security message to relevant KeyguardMessageArea. */ public void setKeyguardMessage(String message, ColorStateList colorState) { - if (isShowingAlternateBouncer()) { + if (mAlternateBouncerInteractor.isVisibleState()) { if (mKeyguardMessageAreaController != null) { mKeyguardMessageAreaController.setMessage(message); } @@ -1421,6 +1445,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb public void dump(PrintWriter pw) { pw.println("StatusBarKeyguardViewManager:"); + pw.println(" mIsModernAlternateBouncerEnabled: " + mIsModernAlternateBouncerEnabled); pw.println(" mRemoteInputActive: " + mRemoteInputActive); pw.println(" mDozing: " + mDozing); pw.println(" mAfterKeyguardGoneAction: " + mAfterKeyguardGoneAction); @@ -1438,9 +1463,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mPrimaryBouncer.dump(pw); } - if (mAlternateBouncer != null) { - pw.println("AlternateBouncer:"); - mAlternateBouncer.dump(pw); + if (mOccludingAppBiometricUI != null) { + pw.println("mOccludingAppBiometricUI:"); + mOccludingAppBiometricUI.dump(pw); } } @@ -1492,14 +1517,17 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb return mPrimaryBouncer; } - public boolean isShowingAlternateBouncer() { - return mAlternateBouncer != null && mAlternateBouncer.isShowingAlternateBouncer(); - } - /** - * Forward touches to callbacks. + * For any touches on the NPVC, show the primary bouncer if the alternate bouncer is currently + * showing. */ public void onTouch(MotionEvent event) { + if (mAlternateBouncerInteractor.isVisibleState() + && mAlternateBouncerInteractor.hasAlternateBouncerShownWithMinTime()) { + showPrimaryBouncer(true); + } + + // Forward NPVC touches to callbacks in case they want to respond to touches for (KeyguardViewManagerCallback callback: mCallbacks) { callback.onTouch(event); } @@ -1542,8 +1570,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb */ public void requestFp(boolean request, int udfpsColor) { mKeyguardUpdateManager.requestFingerprintAuthOnOccludingApp(request); - if (mAlternateBouncer != null) { - mAlternateBouncer.requestUdfps(request, udfpsColor); + if (mOccludingAppBiometricUI != null) { + mOccludingAppBiometricUI.requestUdfps(request, udfpsColor); } } @@ -1614,10 +1642,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } /** - * Delegate used to send show and hide events to an alternate authentication method instead of - * the regular pin/pattern/password bouncer. + * @Deprecated Delegate used to send show and hide events to an alternate bouncer. */ - public interface AlternateBouncer { + public interface LegacyAlternateBouncer { /** * Show alternate authentication bouncer. * @return whether alternate auth method was newly shown @@ -1634,7 +1661,13 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * @return true if the alternate auth bouncer is showing */ boolean isShowingAlternateBouncer(); + } + /** + * Delegate used to send show and hide events to an alternate authentication method instead of + * the regular pin/pattern/password bouncer. + */ + public interface OccludingAppBiometricUI { /** * Use when an app occluding the keyguard would like to give the user ability to * unlock the device using udfps. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt index 6cd8c78dd52f..9e6bb20f429d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt @@ -16,7 +16,9 @@ package com.android.systemui.statusbar.phone +import android.view.InsetsFlags import android.view.InsetsVisibilities +import android.view.ViewDebug import android.view.WindowInsetsController.Appearance import android.view.WindowInsetsController.Behavior import com.android.internal.statusbar.LetterboxDetails @@ -148,4 +150,20 @@ private data class SystemBarAttributesParams( ) { val letterboxesArray = letterboxes.toTypedArray() val appearanceRegionsArray = appearanceRegions.toTypedArray() + override fun toString(): String { + val appearanceToString = + ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance) + return """SystemBarAttributesParams( + displayId=$displayId, + appearance=$appearanceToString, + appearanceRegions=$appearanceRegions, + navbarColorManagedByIme=$navbarColorManagedByIme, + behavior=$behavior, + requestedVisibilities=$requestedVisibilities, + packageName='$packageName', + letterboxes=$letterboxes, + letterboxesArray=${letterboxesArray.contentToString()}, + appearanceRegionsArray=${appearanceRegionsArray.contentToString()} + )""".trimMargin() + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt index 59603874efde..5562e73f0478 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.pipeline.mobile.data.model import android.telephony.Annotation.NetworkType +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy /** @@ -38,4 +40,12 @@ sealed interface ResolvedNetworkType { data class OverrideNetworkType( override val lookupKey: String, ) : ResolvedNetworkType + + /** Represents the carrier merged network. See [CarrierMergedConnectionRepository]. */ + object CarrierMergedNetworkType : ResolvedNetworkType { + // Effectively unused since [iconGroupOverride] is used instead. + override val lookupKey: String = "cwf" + + val iconGroupOverride: SignalIcon.MobileIconGroup = TelephonyIcons.CARRIER_MERGED_WIFI + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index d04996b4d6ce..6187f64e011d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -22,7 +22,6 @@ import android.telephony.TelephonyManager import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow /** @@ -50,7 +49,7 @@ interface MobileConnectionRepository { * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single * listener + model. */ - val connectionInfo: Flow<MobileConnectionModel> + val connectionInfo: StateFlow<MobileConnectionModel> /** The total number of levels. Used with [SignalDrawable]. */ val numberOfLevels: StateFlow<Int> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt index 0e164e7ee859..22aca0a8b0d7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -39,7 +39,11 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConn import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.CarrierMergedConnectionRepository.Companion.createCarrierMergedConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -60,15 +64,19 @@ import kotlinx.coroutines.launch class DemoMobileConnectionsRepository @Inject constructor( - private val dataSource: DemoModeMobileConnectionDataSource, + private val mobileDataSource: DemoModeMobileConnectionDataSource, + private val wifiDataSource: DemoModeWifiDataSource, @Application private val scope: CoroutineScope, context: Context, private val logFactory: TableLogBufferFactory, ) : MobileConnectionsRepository { - private var demoCommandJob: Job? = null + private var mobileDemoCommandJob: Job? = null + private var wifiDemoCommandJob: Job? = null - private var connectionRepoCache = mutableMapOf<Int, DemoMobileConnectionRepository>() + private var carrierMergedSubId: Int? = null + + private var connectionRepoCache = mutableMapOf<Int, CacheContainer>() private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionModel>() val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) @@ -144,52 +152,83 @@ constructor( override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel()) override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository { - return connectionRepoCache[subId] - ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it } + val current = connectionRepoCache[subId]?.repo + if (current != null) { + return current + } + + val new = createDemoMobileConnectionRepo(subId) + connectionRepoCache[subId] = new + return new.repo } - private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository { - val tableLogBuffer = logFactory.create("DemoMobileConnectionLog [$subId]", 100) + private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer { + val tableLogBuffer = + logFactory.getOrCreate( + "DemoMobileConnectionLog [$subId]", + MOBILE_CONNECTION_BUFFER_SIZE, + ) - return DemoMobileConnectionRepository( - subId, - tableLogBuffer, - ) + val repo = + DemoMobileConnectionRepository( + subId, + tableLogBuffer, + ) + return CacheContainer(repo, lastMobileState = null) } override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) fun startProcessingCommands() { - demoCommandJob = + mobileDemoCommandJob = + scope.launch { + mobileDataSource.mobileEvents.filterNotNull().collect { event -> + processMobileEvent(event) + } + } + wifiDemoCommandJob = scope.launch { - dataSource.mobileEvents.filterNotNull().collect { event -> processEvent(event) } + wifiDataSource.wifiEvents.filterNotNull().collect { event -> + processWifiEvent(event) + } } } fun stopProcessingCommands() { - demoCommandJob?.cancel() + mobileDemoCommandJob?.cancel() + wifiDemoCommandJob?.cancel() _subscriptions.value = listOf() connectionRepoCache.clear() subscriptionInfoCache.clear() } - private fun processEvent(event: FakeNetworkEventModel) { + private fun processMobileEvent(event: FakeNetworkEventModel) { when (event) { is Mobile -> { processEnabledMobileState(event) } is MobileDisabled -> { - processDisabledMobileState(event) + maybeRemoveSubscription(event.subId) } } } + private fun processWifiEvent(event: FakeWifiEventModel) { + when (event) { + is FakeWifiEventModel.WifiDisabled -> disableCarrierMerged() + is FakeWifiEventModel.Wifi -> disableCarrierMerged() + is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event) + } + } + private fun processEnabledMobileState(state: Mobile) { // get or create the connection repo, and set its values val subId = state.subId ?: DEFAULT_SUB_ID maybeCreateSubscription(subId) val connection = getRepoForSubId(subId) + connectionRepoCache[subId]?.lastMobileState = state + // This is always true here, because we split out disabled states at the data-source level connection.dataEnabled.value = true connection.networkName.value = NetworkNameModel.Derived(state.name) @@ -198,14 +237,36 @@ constructor( connection.connectionInfo.value = state.toMobileConnectionModel() } - private fun processDisabledMobileState(state: MobileDisabled) { + private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) { + // The new carrier merged connection is for a different sub ID, so disable carrier merged + // for the current (now old) sub + if (carrierMergedSubId != event.subscriptionId) { + disableCarrierMerged() + } + + // get or create the connection repo, and set its values + val subId = event.subscriptionId + maybeCreateSubscription(subId) + carrierMergedSubId = subId + + val connection = getRepoForSubId(subId) + // This is always true here, because we split out disabled states at the data-source level + connection.dataEnabled.value = true + connection.networkName.value = NetworkNameModel.Derived(CARRIER_MERGED_NAME) + connection.numberOfLevels.value = event.numberOfLevels + connection.cdmaRoaming.value = false + connection.connectionInfo.value = event.toMobileConnectionModel() + Log.e("CCS", "output connection info = ${connection.connectionInfo.value}") + } + + private fun maybeRemoveSubscription(subId: Int?) { if (_subscriptions.value.isEmpty()) { // Nothing to do here return } - val subId = - state.subId + val finalSubId = + subId ?: run { // For sake of usability, we can allow for no subId arg if there is only one // subscription @@ -223,7 +284,21 @@ constructor( _subscriptions.value[0].subscriptionId } - removeSubscription(subId) + removeSubscription(finalSubId) + } + + private fun disableCarrierMerged() { + val currentCarrierMergedSubId = carrierMergedSubId ?: return + + // If this sub ID was previously not carrier merged, we should reset it to its previous + // connection. + val lastMobileState = connectionRepoCache[carrierMergedSubId]?.lastMobileState + if (lastMobileState != null) { + processEnabledMobileState(lastMobileState) + } else { + // Otherwise, just remove the subscription entirely + removeSubscription(currentCarrierMergedSubId) + } } private fun removeSubscription(subId: Int) { @@ -251,6 +326,10 @@ constructor( ) } + private fun FakeWifiEventModel.CarrierMerged.toMobileConnectionModel(): MobileConnectionModel { + return createCarrierMergedConnectionModel(this.level) + } + private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType { val key = mobileMappingsReverseLookup.value[this] ?: "dis" return DefaultNetworkType(key) @@ -260,9 +339,17 @@ constructor( private const val TAG = "DemoMobileConnectionsRepo" private const val DEFAULT_SUB_ID = 1 + + private const val CARRIER_MERGED_NAME = "Carrier Merged Network" } } +class CacheContainer( + var repo: DemoMobileConnectionRepository, + /** The last received [Mobile] event. Used when switching from carrier merged back to mobile. */ + var lastMobileState: Mobile?, +) + class DemoMobileConnectionRepository( override val subId: Int, override val tableLogBuffer: TableLogBuffer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt new file mode 100644 index 000000000000..c783b12e0c0b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt @@ -0,0 +1,181 @@ +/* + * 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.statusbar.pipeline.mobile.data.repository.prod + +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * A repository implementation for a carrier merged (aka VCN) network. A carrier merged network is + * delivered to SysUI as a wifi network (see [WifiNetworkModel.CarrierMerged], but is visually + * displayed as a mobile network triangle. + * + * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information. + * + * See [MobileConnectionRepositoryImpl] for a repository implementation of a typical mobile + * connection. + */ +class CarrierMergedConnectionRepository( + override val subId: Int, + override val tableLogBuffer: TableLogBuffer, + defaultNetworkName: NetworkNameModel, + @Application private val scope: CoroutineScope, + val wifiRepository: WifiRepository, +) : MobileConnectionRepository { + + /** + * Outputs the carrier merged network to use, or null if we don't have a valid carrier merged + * network. + */ + private val network: Flow<WifiNetworkModel.CarrierMerged?> = + combine( + wifiRepository.isWifiEnabled, + wifiRepository.isWifiDefault, + wifiRepository.wifiNetwork, + ) { isEnabled, isDefault, network -> + when { + !isEnabled -> null + !isDefault -> null + network !is WifiNetworkModel.CarrierMerged -> null + network.subscriptionId != subId -> { + Log.w( + TAG, + "Connection repo subId=$subId " + + "does not equal wifi repo subId=${network.subscriptionId}; " + + "not showing carrier merged" + ) + null + } + else -> network + } + } + + override val connectionInfo: StateFlow<MobileConnectionModel> = + network + .map { it.toMobileConnectionModel() } + .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel()) + + // TODO(b/238425913): Add logging to this class. + // TODO(b/238425913): Make sure SignalStrength.getEmptyState is used when appropriate. + + // Carrier merged is never roaming. + override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow() + + // TODO(b/238425913): Fetch the carrier merged network name. + override val networkName: StateFlow<NetworkNameModel> = + flowOf(defaultNetworkName) + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName) + + override val numberOfLevels: StateFlow<Int> = + wifiRepository.wifiNetwork + .map { + if (it is WifiNetworkModel.CarrierMerged) { + it.numberOfLevels + } else { + DEFAULT_NUM_LEVELS + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS) + + override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled + + private fun WifiNetworkModel.CarrierMerged?.toMobileConnectionModel(): MobileConnectionModel { + if (this == null) { + return MobileConnectionModel() + } + + return createCarrierMergedConnectionModel(level) + } + + companion object { + /** + * Creates an instance of [MobileConnectionModel] that represents a carrier merged network + * with the given [level]. + */ + fun createCarrierMergedConnectionModel(level: Int): MobileConnectionModel { + return MobileConnectionModel( + primaryLevel = level, + cdmaLevel = level, + // A [WifiNetworkModel.CarrierMerged] instance is always connected. + // (A [WifiNetworkModel.Inactive] represents a disconnected network.) + dataConnectionState = DataConnectionState.Connected, + // TODO(b/238425913): This should come from [WifiRepository.wifiActivity]. + dataActivityDirection = + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ), + resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType, + // Carrier merged is never roaming + isRoaming = false, + + // TODO(b/238425913): Verify that these fields never change for carrier merged. + isEmergencyOnly = false, + operatorAlphaShort = null, + isInService = true, + isGsm = false, + carrierNetworkChangeActive = false, + ) + } + } + + @SysUISingleton + class Factory + @Inject + constructor( + @Application private val scope: CoroutineScope, + private val wifiRepository: WifiRepository, + ) { + fun build( + subId: Int, + mobileLogger: TableLogBuffer, + defaultNetworkName: NetworkNameModel, + ): MobileConnectionRepository { + return CarrierMergedConnectionRepository( + subId, + mobileLogger, + defaultNetworkName, + scope, + wifiRepository, + ) + } + } +} + +private const val TAG = "CarrierMergedConnectionRepository" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt new file mode 100644 index 000000000000..0f30ae249c31 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -0,0 +1,179 @@ +/* + * 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.statusbar.pipeline.mobile.data.repository.prod + +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** + * A repository that fully implements a mobile connection. + * + * This connection could either be a typical mobile connection (see [MobileConnectionRepositoryImpl] + * or a carrier merged connection (see [CarrierMergedConnectionRepository]). This repository + * switches between the two types of connections based on whether the connection is currently + * carrier merged (see [setIsCarrierMerged]). + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class FullMobileConnectionRepository( + override val subId: Int, + startingIsCarrierMerged: Boolean, + override val tableLogBuffer: TableLogBuffer, + private val defaultNetworkName: NetworkNameModel, + private val networkNameSeparator: String, + private val globalMobileDataSettingChangedEvent: Flow<Unit>, + @Application scope: CoroutineScope, + private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory, + private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory, +) : MobileConnectionRepository { + /** + * Sets whether this connection is a typical mobile connection or a carrier merged connection. + */ + fun setIsCarrierMerged(isCarrierMerged: Boolean) { + _isCarrierMerged.value = isCarrierMerged + } + + /** + * Returns true if this repo is currently for a carrier merged connection and false otherwise. + */ + @VisibleForTesting fun getIsCarrierMerged() = _isCarrierMerged.value + + private val _isCarrierMerged = MutableStateFlow(startingIsCarrierMerged) + private val isCarrierMerged: StateFlow<Boolean> = + _isCarrierMerged + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = "isCarrierMerged", + initialValue = startingIsCarrierMerged, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), startingIsCarrierMerged) + + private val mobileRepo: MobileConnectionRepository by lazy { + mobileRepoFactory.build( + subId, + tableLogBuffer, + defaultNetworkName, + networkNameSeparator, + globalMobileDataSettingChangedEvent, + ) + } + + private val carrierMergedRepo: MobileConnectionRepository by lazy { + carrierMergedRepoFactory.build(subId, tableLogBuffer, defaultNetworkName) + } + + @VisibleForTesting + internal val activeRepo: StateFlow<MobileConnectionRepository> = run { + val initial = + if (startingIsCarrierMerged) { + carrierMergedRepo + } else { + mobileRepo + } + + this.isCarrierMerged + .mapLatest { isCarrierMerged -> + if (isCarrierMerged) { + carrierMergedRepo + } else { + mobileRepo + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), initial) + } + + override val cdmaRoaming = + activeRepo + .flatMapLatest { it.cdmaRoaming } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaRoaming.value) + + override val connectionInfo = + activeRepo + .flatMapLatest { it.connectionInfo } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.connectionInfo.value) + + override val dataEnabled = + activeRepo + .flatMapLatest { it.dataEnabled } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value) + + override val numberOfLevels = + activeRepo + .flatMapLatest { it.numberOfLevels } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.numberOfLevels.value) + + override val networkName = + activeRepo + .flatMapLatest { it.networkName } + .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value) + + class Factory + @Inject + constructor( + @Application private val scope: CoroutineScope, + private val logFactory: TableLogBufferFactory, + private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory, + private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory, + ) { + fun build( + subId: Int, + startingIsCarrierMerged: Boolean, + defaultNetworkName: NetworkNameModel, + networkNameSeparator: String, + globalMobileDataSettingChangedEvent: Flow<Unit>, + ): FullMobileConnectionRepository { + val mobileLogger = + logFactory.getOrCreate(tableBufferLogName(subId), MOBILE_CONNECTION_BUFFER_SIZE) + + return FullMobileConnectionRepository( + subId, + startingIsCarrierMerged, + mobileLogger, + defaultNetworkName, + networkNameSeparator, + globalMobileDataSettingChangedEvent, + scope, + mobileRepoFactory, + carrierMergedRepoFactory, + ) + } + + companion object { + /** The buffer size to use for logging. */ + const val MOBILE_CONNECTION_BUFFER_SIZE = 100 + + /** Returns a log buffer name for a mobile connection with the given [subId]. */ + fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]" + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 0fa0fea0bebf..3f2ce4000ff1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -38,7 +38,6 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -70,6 +69,10 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +/** + * A repository implementation for a typical mobile connection (as opposed to a carrier merged + * connection -- see [CarrierMergedConnectionRepository]). + */ @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileConnectionRepositoryImpl( @@ -298,18 +301,16 @@ class MobileConnectionRepositoryImpl( private val logger: ConnectivityPipelineLogger, private val globalSettings: GlobalSettings, private val mobileMappingsProxy: MobileMappingsProxy, - private val logFactory: TableLogBufferFactory, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, ) { fun build( subId: Int, + mobileLogger: TableLogBuffer, defaultNetworkName: NetworkNameModel, networkNameSeparator: String, globalMobileDataSettingChangedEvent: Flow<Unit>, ): MobileConnectionRepository { - val mobileLogger = logFactory.create(tableBufferLogName(subId), 100) - return MobileConnectionRepositoryImpl( context, subId, @@ -327,8 +328,4 @@ class MobileConnectionRepositoryImpl( ) } } - - companion object { - fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]" - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index c88c70064238..4472e0972a0b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -46,11 +46,12 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -85,9 +86,14 @@ constructor( private val context: Context, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, - private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory + // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi + // repository is an input to the mobile repository. + // See [CarrierMergedConnectionRepository] for details. + wifiRepository: WifiRepository, + private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory, ) : MobileConnectionsRepository { - private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf() + private var subIdRepositoryCache: MutableMap<Int, FullMobileConnectionRepository> = + mutableMapOf() private val defaultNetworkName = NetworkNameModel.Default( @@ -97,30 +103,43 @@ constructor( private val networkNameSeparator: String = context.getString(R.string.status_bar_network_name_separator) + private val carrierMergedSubId: StateFlow<Int?> = + wifiRepository.wifiNetwork + .mapLatest { + if (it is WifiNetworkModel.CarrierMerged) { + it.subscriptionId + } else { + null + } + } + .distinctUntilChanged() + .stateIn(scope, started = SharingStarted.WhileSubscribed(), null) + + private val mobileSubscriptionsChangeEvent: Flow<Unit> = conflatedCallbackFlow { + val callback = + object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + trySend(Unit) + } + } + + subscriptionManager.addOnSubscriptionsChangedListener( + bgDispatcher.asExecutor(), + callback, + ) + + awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } + } + /** * State flow that emits the set of mobile data subscriptions, each represented by its own - * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each - * info object, but for now we keep track of the infos themselves. + * [SubscriptionModel]. */ override val subscriptions: StateFlow<List<SubscriptionModel>> = - conflatedCallbackFlow { - val callback = - object : SubscriptionManager.OnSubscriptionsChangedListener() { - override fun onSubscriptionsChanged() { - trySend(Unit) - } - } - - subscriptionManager.addOnSubscriptionsChangedListener( - bgDispatcher.asExecutor(), - callback, - ) - - awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } - } + merge(mobileSubscriptionsChangeEvent, carrierMergedSubId) .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } } .logInputChange(logger, "onSubscriptionsChanged") - .onEach { infos -> dropUnusedReposFromCache(infos) } + .onEach { infos -> updateRepos(infos) } .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) /** StateFlow that keeps track of the current active mobile data subscription */ @@ -173,7 +192,7 @@ constructor( .distinctUntilChanged() .logInputChange(logger, "defaultMobileIconGroup") - override fun getRepoForSubId(subId: Int): MobileConnectionRepository { + override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository { if (!isValidSubId(subId)) { throw IllegalArgumentException( "subscriptionId $subId is not in the list of valid subscriptions" @@ -251,15 +270,27 @@ constructor( @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache - private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { - return mobileConnectionRepositoryFactory.build( + private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository { + return fullMobileRepoFactory.build( subId, + isCarrierMerged(subId), defaultNetworkName, networkNameSeparator, globalMobileDataSettingChangedEvent, ) } + private fun updateRepos(newInfos: List<SubscriptionModel>) { + dropUnusedReposFromCache(newInfos) + subIdRepositoryCache.forEach { (subId, repo) -> + repo.setIsCarrierMerged(isCarrierMerged(subId)) + } + } + + private fun isCarrierMerged(subId: Int): Boolean { + return subId == carrierMergedSubId.value + } + private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) { // Remove any connection repository from the cache that isn't in the new set of IDs. They // will get garbage collected once their subscribers go away diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 9427c6b9fece..003df2482c6e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -22,8 +22,8 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -138,7 +138,11 @@ class MobileIconInteractorImpl( defaultMobileIconMapping, defaultMobileIconGroup, ) { info, mapping, defaultGroup -> - mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup + when (info.resolvedNetworkType) { + is ResolvedNetworkType.CarrierMergedNetworkType -> + info.resolvedNetworkType.iconGroupOverride + else -> mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup + } } .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt index 4251d18357f7..da2daf2c55ea 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt @@ -16,13 +16,18 @@ package com.android.systemui.statusbar.pipeline.wifi.data.model +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.annotation.VisibleForTesting import com.android.systemui.log.table.TableRowLogger import com.android.systemui.log.table.Diffable +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS /** Provides information about the current wifi network. */ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { + // TODO(b/238425913): Have a better, more unified strategy for diff-logging instead of + // copy-pasting the column names for each sub-object. + /** * A model representing that we couldn't fetch any wifi information. * @@ -41,8 +46,43 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { override fun logFull(row: TableRowLogger) { row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE) row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) + row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) + row.logChange(COL_VALIDATED, false) + row.logChange(COL_LEVEL, LEVEL_DEFAULT) + row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) + row.logChange(COL_SSID, null) + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) + } + } + + /** + * A model representing that the wifi information we received was invalid in some way. + */ + data class Invalid( + /** A description of why the wifi information was invalid. */ + val invalidReason: String, + ) : WifiNetworkModel() { + override fun toString() = "WifiNetwork.Invalid[$invalidReason]" + override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { + if (prevVal !is Invalid) { + logFull(row) + return + } + + if (invalidReason != prevVal.invalidReason) { + row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason") + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason") + row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) + row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) row.logChange(COL_VALIDATED, false) row.logChange(COL_LEVEL, LEVEL_DEFAULT) + row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) row.logChange(COL_SSID, null) row.logChange(COL_PASSPOINT_ACCESS_POINT, false) row.logChange(COL_ONLINE_SIGN_UP, false) @@ -59,18 +99,21 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { return } - if (prevVal is CarrierMerged) { - // The only difference between CarrierMerged and Inactive is the type - row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE) - return - } - - // When changing from Active to Inactive, we need to log diffs to all the fields. - logFullNonActiveNetwork(TYPE_INACTIVE, row) + // When changing to Inactive, we need to log diffs to all the fields. + logFull(row) } override fun logFull(row: TableRowLogger) { - logFullNonActiveNetwork(TYPE_INACTIVE, row) + row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE) + row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) + row.logChange(COL_SUB_ID, SUB_ID_DEFAULT) + row.logChange(COL_VALIDATED, false) + row.logChange(COL_LEVEL, LEVEL_DEFAULT) + row.logChange(COL_NUM_LEVELS, NUM_LEVELS_DEFAULT) + row.logChange(COL_SSID, null) + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) } } @@ -80,22 +123,75 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { * * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information. */ - object CarrierMerged : WifiNetworkModel() { - override fun toString() = "WifiNetwork.CarrierMerged" + data class CarrierMerged( + /** + * The [android.net.Network.netId] we received from + * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network. + * + * Importantly, **not** [android.net.wifi.WifiInfo.getNetworkId]. + */ + val networkId: Int, + + /** + * The subscription ID that this connection represents. + * + * Comes from [android.net.wifi.WifiInfo.getSubscriptionId]. + * + * Per that method, this value must not be [INVALID_SUBSCRIPTION_ID] (if it was invalid, + * then this is *not* a carrier merged network). + */ + val subscriptionId: Int, + + /** + * The signal level, guaranteed to be 0 <= level <= numberOfLevels. + */ + val level: Int, + + /** + * The maximum possible level. + */ + val numberOfLevels: Int = DEFAULT_NUM_LEVELS, + ) : WifiNetworkModel() { + init { + require(level in MIN_VALID_LEVEL..numberOfLevels) { + "0 <= wifi level <= $numberOfLevels required; level was $level" + } + require(subscriptionId != INVALID_SUBSCRIPTION_ID) { + "subscription ID cannot be invalid" + } + } override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { - if (prevVal is CarrierMerged) { + if (prevVal !is CarrierMerged) { + logFull(row) return } - if (prevVal is Inactive) { - // The only difference between CarrierMerged and Inactive is the type. - row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED) - return + if (prevVal.networkId != networkId) { + row.logChange(COL_NETWORK_ID, networkId) } + if (prevVal.subscriptionId != subscriptionId) { + row.logChange(COL_SUB_ID, subscriptionId) + } + if (prevVal.level != level) { + row.logChange(COL_LEVEL, level) + } + if (prevVal.numberOfLevels != numberOfLevels) { + row.logChange(COL_NUM_LEVELS, numberOfLevels) + } + } - // When changing from Active to CarrierMerged, we need to log diffs to all the fields. - logFullNonActiveNetwork(TYPE_CARRIER_MERGED, row) + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED) + row.logChange(COL_NETWORK_ID, networkId) + row.logChange(COL_SUB_ID, subscriptionId) + row.logChange(COL_VALIDATED, true) + row.logChange(COL_LEVEL, level) + row.logChange(COL_NUM_LEVELS, numberOfLevels) + row.logChange(COL_SSID, null) + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) } } @@ -137,38 +233,50 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { if (prevVal !is Active) { - row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE) + logFull(row) + return } - if (prevVal !is Active || prevVal.networkId != networkId) { + if (prevVal.networkId != networkId) { row.logChange(COL_NETWORK_ID, networkId) } - if (prevVal !is Active || prevVal.isValidated != isValidated) { + if (prevVal.isValidated != isValidated) { row.logChange(COL_VALIDATED, isValidated) } - if (prevVal !is Active || prevVal.level != level) { + if (prevVal.level != level) { row.logChange(COL_LEVEL, level) } - if (prevVal !is Active || prevVal.ssid != ssid) { + if (prevVal.ssid != ssid) { row.logChange(COL_SSID, ssid) } // TODO(b/238425913): The passpoint-related values are frequently never used, so it // would be great to not log them when they're not used. - if (prevVal !is Active || prevVal.isPasspointAccessPoint != isPasspointAccessPoint) { + if (prevVal.isPasspointAccessPoint != isPasspointAccessPoint) { row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint) } - if (prevVal !is Active || - prevVal.isOnlineSignUpForPasspointAccessPoint != + if (prevVal.isOnlineSignUpForPasspointAccessPoint != isOnlineSignUpForPasspointAccessPoint) { row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint) } - if (prevVal !is Active || - prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) { + if (prevVal.passpointProviderFriendlyName != passpointProviderFriendlyName) { row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName) } } + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_TYPE, TYPE_ACTIVE) + row.logChange(COL_NETWORK_ID, networkId) + row.logChange(COL_SUB_ID, null) + row.logChange(COL_VALIDATED, isValidated) + row.logChange(COL_LEVEL, level) + row.logChange(COL_NUM_LEVELS, null) + row.logChange(COL_SSID, ssid) + row.logChange(COL_PASSPOINT_ACCESS_POINT, isPasspointAccessPoint) + row.logChange(COL_ONLINE_SIGN_UP, isOnlineSignUpForPasspointAccessPoint) + row.logChange(COL_PASSPOINT_NAME, passpointProviderFriendlyName) + } + override fun toString(): String { // Only include the passpoint-related values in the string if we have them. (Most // networks won't have them so they'll be mostly clutter.) @@ -189,21 +297,13 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { companion object { @VisibleForTesting - internal const val MIN_VALID_LEVEL = 0 - @VisibleForTesting internal const val MAX_VALID_LEVEL = 4 } } - internal fun logFullNonActiveNetwork(type: String, row: TableRowLogger) { - row.logChange(COL_NETWORK_TYPE, type) - row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) - row.logChange(COL_VALIDATED, false) - row.logChange(COL_LEVEL, LEVEL_DEFAULT) - row.logChange(COL_SSID, null) - row.logChange(COL_PASSPOINT_ACCESS_POINT, false) - row.logChange(COL_ONLINE_SIGN_UP, false) - row.logChange(COL_PASSPOINT_NAME, null) + companion object { + @VisibleForTesting + internal const val MIN_VALID_LEVEL = 0 } } @@ -214,12 +314,16 @@ const val TYPE_ACTIVE = "Active" const val COL_NETWORK_TYPE = "type" const val COL_NETWORK_ID = "networkId" +const val COL_SUB_ID = "subscriptionId" const val COL_VALIDATED = "isValidated" const val COL_LEVEL = "level" +const val COL_NUM_LEVELS = "maxLevel" const val COL_SSID = "ssid" const val COL_PASSPOINT_ACCESS_POINT = "isPasspointAccessPoint" const val COL_ONLINE_SIGN_UP = "isOnlineSignUpForPasspointAccessPoint" const val COL_PASSPOINT_NAME = "passpointProviderFriendlyName" val LEVEL_DEFAULT: String? = null +val NUM_LEVELS_DEFAULT: String? = null val NETWORK_ID_DEFAULT: String? = null +val SUB_ID_DEFAULT: String? = null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt index c588945fbd67..caac8fa2f2c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK import com.android.systemui.demomode.DemoModeController +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -43,10 +44,10 @@ constructor( private fun Bundle.toWifiEvent(): FakeWifiEventModel? { val wifi = getString("wifi") ?: return null - return if (wifi == "show") { - activeWifiEvent() - } else { - FakeWifiEventModel.WifiDisabled + return when (wifi) { + "show" -> activeWifiEvent() + "carriermerged" -> carrierMergedWifiEvent() + else -> FakeWifiEventModel.WifiDisabled } } @@ -64,6 +65,14 @@ constructor( ) } + private fun Bundle.carrierMergedWifiEvent(): FakeWifiEventModel.CarrierMerged { + val subId = getString("slot")?.toInt() ?: DEFAULT_CARRIER_MERGED_SUB_ID + val level = getString("level")?.toInt() ?: 0 + val numberOfLevels = getString("numlevels")?.toInt() ?: DEFAULT_NUM_LEVELS + + return FakeWifiEventModel.CarrierMerged(subId, level, numberOfLevels) + } + private fun String.toActivity(): Int = when (this) { "inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT @@ -71,4 +80,8 @@ constructor( "out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE } + + companion object { + const val DEFAULT_CARRIER_MERGED_SUB_ID = 10 + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt index be3d7d4e65c4..e161b3e42d02 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt @@ -66,6 +66,7 @@ constructor( private fun processEvent(event: FakeWifiEventModel) = when (event) { is FakeWifiEventModel.Wifi -> processEnabledWifiState(event) + is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event) is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState() } @@ -85,6 +86,14 @@ constructor( _wifiNetwork.value = event.toWifiNetworkModel() } + private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) { + _isWifiEnabled.value = true + _isWifiDefault.value = true + // TODO(b/238425913): Support activity in demo mode. + _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false) + _wifiNetwork.value = event.toCarrierMergedModel() + } + private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel = WifiNetworkModel.Active( networkId = DEMO_NET_ID, @@ -99,6 +108,14 @@ constructor( passpointProviderFriendlyName = null, ) + private fun FakeWifiEventModel.CarrierMerged.toCarrierMergedModel(): WifiNetworkModel = + WifiNetworkModel.CarrierMerged( + networkId = DEMO_NET_ID, + subscriptionId = subscriptionId, + level = level, + numberOfLevels = numberOfLevels, + ) + companion object { private const val DEMO_NET_ID = 1234 } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt index 2353fb82f3b1..518f8ce66d2e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt @@ -29,5 +29,11 @@ sealed interface FakeWifiEventModel { val validated: Boolean?, ) : FakeWifiEventModel + data class CarrierMerged( + val subscriptionId: Int, + val level: Int, + val numberOfLevels: Int, + ) : FakeWifiEventModel + object WifiDisabled : FakeWifiEventModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index c47c20d280c7..d26499c18661 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -29,6 +29,7 @@ import android.net.NetworkRequest import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import com.android.settingslib.Utils import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow @@ -269,7 +270,19 @@ constructor( wifiManager: WifiManager, ): WifiNetworkModel { return if (wifiInfo.isCarrierMerged) { - WifiNetworkModel.CarrierMerged + if (wifiInfo.subscriptionId == INVALID_SUBSCRIPTION_ID) { + WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON) + } else { + WifiNetworkModel.CarrierMerged( + networkId = network.getNetId(), + subscriptionId = wifiInfo.subscriptionId, + level = wifiManager.calculateSignalLevel(wifiInfo.rssi), + // The WiFi signal level returned by WifiManager#calculateSignalLevel start + // from 0, so WifiManager#getMaxSignalLevel + 1 represents the total level + // buckets count. + numberOfLevels = wifiManager.maxSignalLevel + 1, + ) + } } else { WifiNetworkModel.Active( network.getNetId(), @@ -302,6 +315,9 @@ constructor( .build() private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel" + + private const val CARRIER_MERGED_INVALID_SUB_ID_REASON = + "Wifi network was carrier merged but had invalid sub ID" } @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt index 980560ab5d58..86dcd18c643c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt @@ -66,6 +66,7 @@ class WifiInteractorImpl @Inject constructor( override val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info -> when (info) { is WifiNetworkModel.Unavailable -> null + is WifiNetworkModel.Invalid -> null is WifiNetworkModel.Inactive -> null is WifiNetworkModel.CarrierMerged -> null is WifiNetworkModel.Active -> when { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index 824b5972ba4b..95431afb71bb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -83,6 +83,7 @@ constructor( private fun WifiNetworkModel.icon(): WifiIcon { return when (this) { is WifiNetworkModel.Unavailable -> WifiIcon.Hidden + is WifiNetworkModel.Invalid -> WifiIcon.Hidden is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden is WifiNetworkModel.Inactive -> WifiIcon.Visible( res = WIFI_NO_NETWORK, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index c9ed0cb4155d..f8c17e8c8379 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -109,6 +109,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33; private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83; private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f; + private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120; + private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180; public final Object mToken = new Object(); @@ -421,7 +423,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } @VisibleForTesting - void onDefocus(boolean animate, boolean logClose) { + void onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus) { mController.removeRemoteInput(mEntry, mToken); mEntry.remoteInputText = mEditText.getText(); @@ -431,18 +433,20 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene ViewGroup parent = (ViewGroup) getParent(); if (animate && parent != null && mIsFocusAnimationFlagActive) { - ViewGroup grandParent = (ViewGroup) parent.getParent(); ViewGroupOverlay overlay = parent.getOverlay(); + View actionsContainer = getActionsContainerLayout(); + int actionsContainerHeight = + actionsContainer != null ? actionsContainer.getHeight() : 0; // After adding this RemoteInputView to the overlay of the parent (and thus removing // it from the parent itself), the parent will shrink in height. This causes the // overlay to be moved. To correct the position of the overlay we need to offset it. - int overlayOffsetY = getMaxSiblingHeight() - getHeight(); + int overlayOffsetY = actionsContainerHeight - getHeight(); overlay.add(this); if (grandParent != null) grandParent.setClipChildren(false); - Animator animator = getDefocusAnimator(overlayOffsetY); + Animator animator = getDefocusAnimator(actionsContainer, overlayOffsetY); View self = this; animator.addListener(new AnimatorListenerAdapter() { @Override @@ -454,8 +458,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene if (mWrapper != null) { mWrapper.setRemoteInputVisible(false); } + if (doAfterDefocus != null) { + doAfterDefocus.run(); + } } }); + if (actionsContainer != null) actionsContainer.setAlpha(0f); animator.start(); } else if (animate && mRevealParams != null && mRevealParams.radius > 0) { @@ -474,6 +482,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene reveal.start(); } else { setVisibility(GONE); + if (doAfterDefocus != null) doAfterDefocus.run(); if (mWrapper != null) { mWrapper.setRemoteInputVisible(false); } @@ -596,10 +605,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene /** * Focuses the RemoteInputView and animates its appearance - * - * @param crossFadeView view that will be crossfaded during the appearance animation */ - public void focusAnimated(View crossFadeView) { + public void focusAnimated() { if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE && mRevealParams != null) { android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this); @@ -609,7 +616,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) { mIsAnimatingAppearance = true; setAlpha(0f); - Animator focusAnimator = getFocusAnimator(crossFadeView); + Animator focusAnimator = getFocusAnimator(getActionsContainerLayout()); focusAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation, boolean isReverse) { @@ -661,6 +668,23 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void reset() { + if (mIsFocusAnimationFlagActive) { + mProgressBar.setVisibility(INVISIBLE); + mResetting = true; + mSending = false; + onDefocus(true /* animate */, false /* logClose */, () -> { + mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); + mEditText.getText().clear(); + mEditText.setEnabled(isAggregatedVisible()); + mSendButton.setVisibility(VISIBLE); + mController.removeSpinning(mEntry.getKey(), mToken); + updateSendButton(); + setAttachment(null); + mResetting = false; + }); + return; + } + mResetting = true; mSending = false; mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); @@ -671,7 +695,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mProgressBar.setVisibility(INVISIBLE); mController.removeSpinning(mEntry.getKey(), mToken); updateSendButton(); - onDefocus(false /* animate */, false /* logClose */); + onDefocus(false /* animate */, false /* logClose */, null /* doAfterDefocus */); setAttachment(null); mResetting = false; @@ -825,23 +849,22 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } /** - * @return max sibling height (0 in case of no siblings) + * @return action button container view (i.e. ViewGroup containing Reply button etc.) */ - public int getMaxSiblingHeight() { + public View getActionsContainerLayout() { ViewGroup parentView = (ViewGroup) getParent(); - int maxHeight = 0; - if (parentView == null) return 0; - for (int i = 0; i < parentView.getChildCount(); i++) { - View siblingView = parentView.getChildAt(i); - if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight()); - } - return maxHeight; + if (parentView == null) return null; + return parentView.findViewById(com.android.internal.R.id.actions_container_layout); } /** * Creates an animator for the focus animation. + * + * @param fadeOutView View that will be faded out during the focus animation. */ - private Animator getFocusAnimator(View crossFadeView) { + private Animator getFocusAnimator(@Nullable View fadeOutView) { + final AnimatorSet animatorSet = new AnimatorSet(); + final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f); alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY); alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); @@ -854,30 +877,36 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); - final Animator crossFadeViewAlphaAnimator = - ObjectAnimator.ofFloat(crossFadeView, View.ALPHA, 1f, 0f); - crossFadeViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); - crossFadeViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); - alphaAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation, boolean isReverse) { - crossFadeView.setAlpha(1f); - } - }); - - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(alphaAnimator, scaleAnimator, crossFadeViewAlphaAnimator); + if (fadeOutView == null) { + animatorSet.playTogether(alphaAnimator, scaleAnimator); + } else { + final Animator fadeOutViewAlphaAnimator = + ObjectAnimator.ofFloat(fadeOutView, View.ALPHA, 1f, 0f); + fadeOutViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); + fadeOutViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation, boolean isReverse) { + fadeOutView.setAlpha(1f); + } + }); + animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeOutViewAlphaAnimator); + } return animatorSet; } /** * Creates an animator for the defocus animation. * - * @param offsetY The RemoteInputView will be offset by offsetY during the animation + * @param fadeInView View that will be faded in during the defocus animation. + * @param offsetY The RemoteInputView will be offset by offsetY during the animation */ - private Animator getDefocusAnimator(int offsetY) { + private Animator getDefocusAnimator(@Nullable View fadeInView, int offsetY) { + final AnimatorSet animatorSet = new AnimatorSet(); + final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f); - alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); + alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); + alphaAnimator.setStartDelay(DEFOCUS_ANIMATION_FADE_OUT_DELAY); alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE); @@ -893,8 +922,17 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } }); - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(alphaAnimator, scaleAnimator); + if (fadeInView == null) { + animatorSet.playTogether(alphaAnimator, scaleAnimator); + } else { + fadeInView.forceHasOverlappingRendering(false); + Animator fadeInViewAlphaAnimator = + ObjectAnimator.ofFloat(fadeInView, View.ALPHA, 0f, 1f); + fadeInViewAlphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); + fadeInViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); + fadeInViewAlphaAnimator.setStartDelay(DEFOCUS_ANIMATION_CROSSFADE_DELAY); + animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeInViewAlphaAnimator); + } return animatorSet; } @@ -1011,7 +1049,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene if (isFocusable() && isEnabled()) { setInnerFocusable(false); if (mRemoteInputView != null) { - mRemoteInputView.onDefocus(animate, true /* logClose */); + mRemoteInputView + .onDefocus(animate, true /* logClose */, null /* doAfterDefocus */); } mShowImeOnInputConnection = false; } diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt deleted file mode 100644 index 154c6e2e3158..000000000000 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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.stylus - -import android.content.Context -import android.hardware.BatteryState -import android.hardware.input.InputManager -import android.os.Handler -import android.util.Log -import android.view.InputDevice -import androidx.annotation.VisibleForTesting -import com.android.systemui.CoreStartable -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import java.util.concurrent.Executor -import javax.inject.Inject - -/** - * A listener that detects when a stylus has first been used, by detecting 1) the presence of an - * internal SOURCE_STYLUS with a battery, or 2) any added SOURCE_STYLUS device with a bluetooth - * address. - */ -@SysUISingleton -class StylusFirstUsageListener -@Inject -constructor( - private val context: Context, - private val inputManager: InputManager, - private val stylusManager: StylusManager, - private val featureFlags: FeatureFlags, - @Background private val executor: Executor, - @Background private val handler: Handler, -) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener { - - // Set must be only accessed from the background handler, which is the same handler that - // runs the StylusManager callbacks. - private val internalStylusDeviceIds: MutableSet<Int> = mutableSetOf() - @VisibleForTesting var hasStarted = false - - override fun start() { - if (true) return // TODO(b/261826950): remove on main - if (hasStarted) return - if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return - if (inputManager.isStylusEverUsed(context)) return - if (!hostDeviceSupportsStylusInput()) return - - hasStarted = true - inputManager.inputDeviceIds.forEach(this::onStylusAdded) - stylusManager.registerCallback(this) - stylusManager.startListener() - } - - override fun onStylusAdded(deviceId: Int) { - if (!hasStarted) return - - val device = inputManager.getInputDevice(deviceId) ?: return - if (device.isExternal || !device.supportsSource(InputDevice.SOURCE_STYLUS)) return - - try { - inputManager.addInputDeviceBatteryListener(deviceId, executor, this) - internalStylusDeviceIds += deviceId - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to register battery listener for $deviceId ${device.name}.") - } - } - - override fun onStylusRemoved(deviceId: Int) { - if (!hasStarted) return - - if (!internalStylusDeviceIds.contains(deviceId)) return - try { - inputManager.removeInputDeviceBatteryListener(deviceId, this) - internalStylusDeviceIds.remove(deviceId) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.") - } - } - - override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) { - if (!hasStarted) return - - onRemoteDeviceFound() - } - - override fun onBatteryStateChanged( - deviceId: Int, - eventTimeMillis: Long, - batteryState: BatteryState - ) { - if (!hasStarted) return - - if (batteryState.isPresent) { - onRemoteDeviceFound() - } - } - - private fun onRemoteDeviceFound() { - inputManager.setStylusEverUsed(context, true) - cleanupListeners() - } - - private fun cleanupListeners() { - stylusManager.unregisterCallback(this) - handler.post { - internalStylusDeviceIds.forEach { - inputManager.removeInputDeviceBatteryListener(it, this) - } - } - } - - private fun hostDeviceSupportsStylusInput(): Boolean { - return inputManager.inputDeviceIds - .asSequence() - .mapNotNull { inputManager.getInputDevice(it) } - .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal } - } - - companion object { - private val TAG = StylusFirstUsageListener::class.simpleName.orEmpty() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index 302d6a9ca1b7..235495cfa50d 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -18,6 +18,8 @@ package com.android.systemui.stylus import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.content.Context +import android.hardware.BatteryState import android.hardware.input.InputManager import android.os.Handler import android.util.ArrayMap @@ -25,6 +27,8 @@ import android.util.Log import android.view.InputDevice import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.Executor import javax.inject.Inject @@ -37,25 +41,37 @@ import javax.inject.Inject class StylusManager @Inject constructor( + private val context: Context, private val inputManager: InputManager, private val bluetoothAdapter: BluetoothAdapter?, @Background private val handler: Handler, @Background private val executor: Executor, -) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener { + private val featureFlags: FeatureFlags, +) : + InputManager.InputDeviceListener, + InputManager.InputDeviceBatteryListener, + BluetoothAdapter.OnMetadataChangedListener { private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList() private val stylusBatteryCallbacks: CopyOnWriteArrayList<StylusBatteryCallback> = CopyOnWriteArrayList() // This map should only be accessed on the handler private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap() + // This variable should only be accessed on the handler + private var hasStarted: Boolean = false /** * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot * at time of starting. */ fun startListener() { - addExistingStylusToMap() - inputManager.registerInputDeviceListener(this, handler) + handler.post { + if (hasStarted) return@post + hasStarted = true + addExistingStylusToMap() + + inputManager.registerInputDeviceListener(this, handler) + } } /** Registers a StylusCallback to listen to stylus events. */ @@ -77,21 +93,30 @@ constructor( } override fun onInputDeviceAdded(deviceId: Int) { + if (!hasStarted) return + val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return + if (!device.isExternal) { + registerBatteryListener(deviceId) + } + // TODO(b/257936830): get address once input api available val btAddress: String? = null inputDeviceAddressMap[deviceId] = btAddress executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) } if (btAddress != null) { + onStylusUsed() onStylusBluetoothConnected(btAddress) executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) } } } override fun onInputDeviceChanged(deviceId: Int) { + if (!hasStarted) return + val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return @@ -112,7 +137,10 @@ constructor( } override fun onInputDeviceRemoved(deviceId: Int) { + if (!hasStarted) return + if (!inputDeviceAddressMap.contains(deviceId)) return + unregisterBatteryListener(deviceId) val btAddress: String? = inputDeviceAddressMap[deviceId] inputDeviceAddressMap.remove(deviceId) @@ -124,13 +152,14 @@ constructor( } override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) { - handler.post executeMetadataChanged@{ - if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) - return@executeMetadataChanged + handler.post { + if (!hasStarted) return@post + + if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) return@post val inputDeviceId: Int = inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull() - ?: return@executeMetadataChanged + ?: return@post val isCharging = String(value) == "true" @@ -140,6 +169,24 @@ constructor( } } + override fun onBatteryStateChanged( + deviceId: Int, + eventTimeMillis: Long, + batteryState: BatteryState + ) { + handler.post { + if (!hasStarted) return@post + + if (batteryState.isPresent) { + onStylusUsed() + } + + executeStylusBatteryCallbacks { cb -> + cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState) + } + } + } + private fun onStylusBluetoothConnected(btAddress: String) { val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return try { @@ -158,6 +205,21 @@ constructor( } } + /** + * An InputDevice that supports [InputDevice.SOURCE_STYLUS] may still be present even when a + * physical stylus device has never been used. This method is run when 1) a USI stylus battery + * event happens, or 2) a bluetooth stylus is connected, as they are both indicators that a + * physical stylus device has actually been used. + */ + private fun onStylusUsed() { + if (true) return // TODO(b/261826950): remove on main + if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return + if (inputManager.isStylusEverUsed(context)) return + + inputManager.setStylusEverUsed(context, true) + executeStylusCallbacks { cb -> cb.onStylusFirstUsed() } + } + private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) { stylusCallbacks.forEach(run) } @@ -166,31 +228,69 @@ constructor( stylusBatteryCallbacks.forEach(run) } + private fun registerBatteryListener(deviceId: Int) { + try { + inputManager.addInputDeviceBatteryListener(deviceId, executor, this) + } catch (e: SecurityException) { + Log.e(TAG, "$e: Failed to register battery listener for $deviceId.") + } + } + + private fun unregisterBatteryListener(deviceId: Int) { + // If deviceId wasn't registered, the result is a no-op, so an "is registered" + // check is not needed. + try { + inputManager.removeInputDeviceBatteryListener(deviceId, this) + } catch (e: SecurityException) { + Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.") + } + } + private fun addExistingStylusToMap() { for (deviceId: Int in inputManager.inputDeviceIds) { val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue if (device.supportsSource(InputDevice.SOURCE_STYLUS)) { // TODO(b/257936830): get address once input api available inputDeviceAddressMap[deviceId] = null + + if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available + // For most devices, an active (non-bluetooth) stylus is represented by an + // internal InputDevice. This InputDevice will be present in InputManager + // before CoreStartables run, and will not be removed. + // In many cases, it reports the battery level of the stylus. + registerBatteryListener(deviceId) + } } } } - /** Callback interface to receive events from the StylusManager. */ + /** + * Callback interface to receive events from the StylusManager. All callbacks are run on the + * same background handler. + */ interface StylusCallback { fun onStylusAdded(deviceId: Int) {} fun onStylusRemoved(deviceId: Int) {} fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {} fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {} + fun onStylusFirstUsed() {} } - /** Callback interface to receive stylus battery events from the StylusManager. */ + /** + * Callback interface to receive stylus battery events from the StylusManager. All callbacks are + * runs on the same background handler. + */ interface StylusBatteryCallback { fun onStylusBluetoothChargingStateChanged( inputDeviceId: Int, btDevice: BluetoothDevice, isCharging: Boolean ) {} + fun onStylusUsiBatteryStateChanged( + deviceId: Int, + eventTimeMillis: Long, + batteryState: BatteryState, + ) {} } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt index 11233dda165c..5a8850a9f89b 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt @@ -18,14 +18,11 @@ package com.android.systemui.stylus import android.hardware.BatteryState import android.hardware.input.InputManager -import android.util.Log import android.view.InputDevice import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import java.util.concurrent.Executor import javax.inject.Inject /** @@ -40,16 +37,7 @@ constructor( private val inputManager: InputManager, private val stylusUsiPowerUi: StylusUsiPowerUI, private val featureFlags: FeatureFlags, - @Background private val executor: Executor, -) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener { - - override fun onStylusAdded(deviceId: Int) { - val device = inputManager.getInputDevice(deviceId) ?: return - - if (!device.isExternal) { - registerBatteryListener(deviceId) - } - } +) : CoreStartable, StylusManager.StylusCallback, StylusManager.StylusBatteryCallback { override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) { stylusUsiPowerUi.refresh() @@ -59,57 +47,30 @@ constructor( stylusUsiPowerUi.refresh() } - override fun onStylusRemoved(deviceId: Int) { - val device = inputManager.getInputDevice(deviceId) ?: return - - if (!device.isExternal) { - unregisterBatteryListener(deviceId) - } - } - - override fun onBatteryStateChanged( + override fun onStylusUsiBatteryStateChanged( deviceId: Int, eventTimeMillis: Long, batteryState: BatteryState ) { - if (batteryState.isPresent) { - stylusUsiPowerUi.updateBatteryState(batteryState) - } - } - - private fun registerBatteryListener(deviceId: Int) { - try { - inputManager.addInputDeviceBatteryListener(deviceId, executor, this) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to register battery listener for $deviceId.") - } - } - - private fun unregisterBatteryListener(deviceId: Int) { - try { - inputManager.removeInputDeviceBatteryListener(deviceId, this) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to unregister battery listener for $deviceId.") + if (batteryState.isPresent && batteryState.capacity > 0f) { + stylusUsiPowerUi.updateBatteryState(deviceId, batteryState) } } override fun start() { if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return - addBatteryListenerForInternalStyluses() + if (!hostDeviceSupportsStylusInput()) return + stylusUsiPowerUi.init() stylusManager.registerCallback(this) stylusManager.startListener() } - private fun addBatteryListenerForInternalStyluses() { - // For most devices, an active stylus is represented by an internal InputDevice. - // This InputDevice will be present in InputManager before CoreStartables run, - // and will not be removed. In many cases, it reports the battery level of the stylus. - inputManager.inputDeviceIds + private fun hostDeviceSupportsStylusInput(): Boolean { + return inputManager.inputDeviceIds .asSequence() .mapNotNull { inputManager.getInputDevice(it) } - .filter { it.supportsSource(InputDevice.SOURCE_STYLUS) } - .forEach { onStylusAdded(it.id) } + .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt index 70a5b366263e..8d5e01c5b782 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt @@ -18,17 +18,21 @@ package com.android.systemui.stylus import android.Manifest import android.app.PendingIntent +import android.content.ActivityNotFoundException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.hardware.BatteryState import android.hardware.input.InputManager +import android.os.Bundle import android.os.Handler import android.os.UserHandle +import android.util.Log import android.view.InputDevice import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -53,6 +57,7 @@ constructor( // These values must only be accessed on the handler. private var batteryCapacity = 1.0f private var suppressed = false + private var inputDeviceId: Int? = null fun init() { val filter = @@ -87,10 +92,12 @@ constructor( } } - fun updateBatteryState(batteryState: BatteryState) { + fun updateBatteryState(deviceId: Int, batteryState: BatteryState) { handler.post updateBattery@{ - if (batteryState.capacity == batteryCapacity) return@updateBattery + if (batteryState.capacity == batteryCapacity || batteryState.capacity <= 0f) + return@updateBattery + inputDeviceId = deviceId batteryCapacity = batteryState.capacity refresh() } @@ -123,13 +130,13 @@ constructor( .setSmallIcon(R.drawable.ic_power_low) .setDeleteIntent(getPendingBroadcast(ACTION_DISMISSED_LOW_BATTERY)) .setContentIntent(getPendingBroadcast(ACTION_CLICKED_LOW_BATTERY)) - .setContentTitle(context.getString(R.string.stylus_battery_low)) - .setContentText( + .setContentTitle( context.getString( - R.string.battery_low_percent_format, + R.string.stylus_battery_low_percentage, NumberFormat.getPercentInstance().format(batteryCapacity) ) ) + .setContentText(context.getString(R.string.stylus_battery_low_subtitle)) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setLocalOnly(true) .setAutoCancel(true) @@ -150,23 +157,41 @@ constructor( } private fun getPendingBroadcast(action: String): PendingIntent? { - return PendingIntent.getBroadcastAsUser( + return PendingIntent.getBroadcast( context, 0, - Intent(action), + Intent(action).setPackage(context.packageName), PendingIntent.FLAG_IMMUTABLE, - UserHandle.CURRENT ) } - private val receiver: BroadcastReceiver = + @VisibleForTesting + internal val receiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { ACTION_DISMISSED_LOW_BATTERY -> updateSuppression(true) ACTION_CLICKED_LOW_BATTERY -> { updateSuppression(true) - // TODO(b/261584943): open USI device details page + if (inputDeviceId == null) return + + val args = Bundle() + args.putInt(KEY_DEVICE_INPUT_ID, inputDeviceId!!) + try { + context.startActivity( + Intent(ACTION_STYLUS_USI_DETAILS) + .putExtra(KEY_SETTINGS_FRAGMENT_ARGS, args) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } catch (e: ActivityNotFoundException) { + // In the rare scenario where the Settings app manifest doesn't contain + // the USI details activity, ignore the intent. + Log.e( + StylusUsiPowerUI::class.java.simpleName, + "Cannot open USI details page." + ) + } } } } @@ -177,9 +202,13 @@ constructor( // https://source.chromium.org/chromium/chromium/src/+/main:ash/system/power/peripheral_battery_notifier.cc;l=41 private const val LOW_BATTERY_THRESHOLD = 0.16f - private val USI_NOTIFICATION_ID = R.string.stylus_battery_low + private val USI_NOTIFICATION_ID = R.string.stylus_battery_low_percentage - private const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss" - private const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click" + @VisibleForTesting const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss" + @VisibleForTesting const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click" + @VisibleForTesting + const val ACTION_STYLUS_USI_DETAILS = "com.android.settings.STYLUS_USI_DETAILS_SETTINGS" + @VisibleForTesting const val KEY_DEVICE_INPUT_ID = "device_input_id" + @VisibleForTesting const val KEY_SETTINGS_FRAGMENT_ARGS = ":settings:show_fragment_args" } } diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt index 59ad24a3e7bb..2709da38a7d8 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt @@ -17,6 +17,9 @@ package com.android.systemui.unfold import android.content.Context +import android.hardware.devicestate.DeviceStateManager +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.LifecycleScreenStatusProvider import com.android.systemui.unfold.config.UnfoldTransitionConfig import com.android.systemui.unfold.system.SystemUnfoldSharedModule @@ -32,6 +35,7 @@ import dagger.Lazy import dagger.Module import dagger.Provides import java.util.Optional +import java.util.concurrent.Executor import javax.inject.Named import javax.inject.Singleton @@ -40,6 +44,20 @@ class UnfoldTransitionModule { @Provides @UnfoldTransitionATracePrefix fun tracingTagPrefix() = "systemui" + /** A globally available FoldStateListener that allows one to query the fold state. */ + @Provides + @Singleton + fun providesFoldStateListener( + deviceStateManager: DeviceStateManager, + @Application context: Context, + @Main executor: Executor + ): DeviceStateManager.FoldStateListener { + val listener = DeviceStateManager.FoldStateListener(context) + deviceStateManager.registerCallback(executor, listener) + + return listener + } + @Provides @Singleton fun providesFoldStateLoggingProvider( diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java index 254f9531ef83..8dc1e8fba600 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java @@ -16,6 +16,7 @@ package com.android.keyguard; +import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; @@ -189,6 +190,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -198,6 +200,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -212,6 +215,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -223,6 +227,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 13cd328d00e0..df6752a2b69d 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -32,6 +32,9 @@ import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_CANCELL import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.HAL_POWER_PRESS_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import static com.google.common.truth.Truth.assertThat; @@ -92,6 +95,7 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.service.dreams.IDreamManager; import android.service.trust.TrustAgentService; import android.telephony.ServiceState; @@ -116,6 +120,7 @@ import com.android.keyguard.KeyguardUpdateMonitor.BiometricAuthenticated; import com.android.keyguard.logging.KeyguardUpdateMonitorLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.log.SessionTracker; @@ -123,6 +128,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; +import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.settings.SecureSettings; @@ -142,6 +148,7 @@ import org.mockito.internal.util.reflection.FieldSetter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; @@ -191,6 +198,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private DevicePolicyManager mDevicePolicyManager; @Mock + private DevicePostureController mDevicePostureController; + @Mock private IDreamManager mDreamManager; @Mock private KeyguardBypassController mKeyguardBypassController; @@ -233,6 +242,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private GlobalSettings mGlobalSettings; private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; + @Mock + private FingerprintInteractiveToAuthProvider mInteractiveToAuthProvider; private final int mCurrentUserId = 100; @@ -296,6 +307,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .thenReturn(new ServiceState()); when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings); when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false); + when(mDevicePostureController.getDevicePosture()).thenReturn(DEVICE_POSTURE_UNKNOWN); mMockitoSession = ExtendedMockito.mockitoSession() .spyStatic(SubscriptionManager.class) @@ -307,6 +319,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mUserTracker.getUserId()).thenReturn(mCurrentUserId); ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService); + mContext.getOrCreateTestableResources().addOverride( + com.android.systemui.R.integer.config_face_auth_supported_posture, + DEVICE_POSTURE_UNKNOWN); mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig( mContext.getResources(), mGlobalSettings, @@ -1250,7 +1265,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void testStartsListeningForSfps_whenKeyguardIsVisible_ifRequireScreenOnToAuthEnabled() + public void startsListeningForSfps_whenKeyguardIsVisible_ifRequireInteractiveToAuthEnabled() throws RemoteException { // SFPS supported and enrolled final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); @@ -1258,12 +1273,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mAuthController.getSfpsProps()).thenReturn(props); when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - // WHEN require screen on to auth is disabled, and keyguard is not awake - when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(0); - mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref(); - - mContext.getOrCreateTestableResources().addOverride( - com.android.internal.R.bool.config_requireScreenOnToAuthEnabled, true); + // WHEN require interactive to auth is disabled, and keyguard is not awake + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false); // Preconditions for sfps auth to run keyguardNotGoingAway(); @@ -1279,9 +1290,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // THEN we should listen for sfps when screen off, because require screen on is disabled assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); - // WHEN require screen on to auth is enabled, and keyguard is not awake - when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(1); - mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref(); + // WHEN require interactive to auth is enabled, and keyguard is not awake + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true); // THEN we shouldn't listen for sfps when screen off, because require screen on is enabled assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); @@ -1295,6 +1305,61 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); } + @Test + public void notListeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthEnabled() + throws RemoteException { + // GIVEN SFPS supported and enrolled + final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); + props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + + // GIVEN Preconditions for sfps auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + statusBarShadeIsLocked(); + + // WHEN require interactive to auth is enabled & keyguard is going to sleep + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true); + deviceGoingToSleep(); + + mTestableLooper.processAllMessages(); + + // THEN we should NOT listen for sfps because device is going to sleep + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); + } + + @Test + public void listeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthDisabled() + throws RemoteException { + // GIVEN SFPS supported and enrolled + final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); + props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + + // GIVEN Preconditions for sfps auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + statusBarShadeIsLocked(); + + // WHEN require interactive to auth is disabled & keyguard is going to sleep + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false); + deviceGoingToSleep(); + + mTestableLooper.processAllMessages(); + + // THEN we should listen for sfps because screen on to auth is disabled + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); + } private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal( @FingerprintSensorProperties.SensorType int sensorType) { @@ -2187,6 +2252,54 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { eq(true)); } + @Test + public void testShouldListenForFace_withAuthSupportPostureConfig_returnsTrue() + throws RemoteException { + mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_CLOSED; + keyguardNotGoingAway(); + bouncerFullyVisibleAndNotGoingToSleep(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + supportsFaceDetection(); + + deviceInPostureStateOpened(); + mTestableLooper.processAllMessages(); + // Should not listen for face when posture state in DEVICE_POSTURE_OPENED + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); + + deviceInPostureStateClosed(); + mTestableLooper.processAllMessages(); + // Should listen for face when posture state in DEVICE_POSTURE_CLOSED + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + } + + @Test + public void testShouldListenForFace_withoutAuthSupportPostureConfig_returnsTrue() + throws RemoteException { + mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_UNKNOWN; + keyguardNotGoingAway(); + bouncerFullyVisibleAndNotGoingToSleep(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + supportsFaceDetection(); + + deviceInPostureStateClosed(); + mTestableLooper.processAllMessages(); + // Whether device in any posture state, always listen for face + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + + deviceInPostureStateOpened(); + mTestableLooper.processAllMessages(); + // Whether device in any posture state, always listen for face + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + } + private void userDeviceLockDown() { when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); when(mStrongAuthTracker.getStrongAuthForUser(mCurrentUserId)) @@ -2266,6 +2379,14 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .onAuthenticationAcquired(FINGERPRINT_ACQUIRED_START); } + private void deviceInPostureStateOpened() { + mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_OPENED); + } + + private void deviceInPostureStateClosed() { + mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_CLOSED); + } + private void successfulFingerprintAuth() { mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback .onAuthenticationSucceeded( @@ -2407,7 +2528,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mPowerManager, mTrustManager, mSubscriptionManager, mUserManager, mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager, mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager, - mFaceWakeUpTriggersConfig); + mFaceWakeUpTriggersConfig, mDevicePostureController, + Optional.of(mInteractiveToAuthProvider)); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); } 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 83bf1834989b..ace0ccb6a25b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -739,7 +739,7 @@ public class AuthControllerTest extends SysuiTestCase { public void testForwardsDozeEvents() throws RemoteException { when(mStatusBarStateController.isDozing()).thenReturn(true); when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE); - mAuthController.setBiometicContextListener(mContextListener); + mAuthController.setBiometricContextListener(mContextListener); mStatusBarStateListenerCaptor.getValue().onDozingChanged(true); mStatusBarStateListenerCaptor.getValue().onDozingChanged(false); @@ -754,7 +754,7 @@ public class AuthControllerTest extends SysuiTestCase { public void testForwardsWakeEvents() throws RemoteException { when(mStatusBarStateController.isDozing()).thenReturn(false); when(mWakefulnessLifecycle.getWakefulness()).thenReturn(WAKEFULNESS_AWAKE); - mAuthController.setBiometicContextListener(mContextListener); + mAuthController.setBiometricContextListener(mContextListener); mWakefullnessObserverCaptor.getValue().onStartedGoingToSleep(); mWakefullnessObserverCaptor.getValue().onFinishedGoingToSleep(); 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 53bc2c231d0c..1ef119d7fb16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -43,6 +43,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager @@ -52,7 +53,6 @@ import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.time.SystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule @@ -95,7 +95,6 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var dumpManager: DumpManager @Mock private lateinit var transitionController: LockscreenShadeTransitionController @Mock private lateinit var configurationController: ConfigurationController - @Mock private lateinit var systemClock: SystemClock @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController @@ -106,7 +105,8 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var udfpsEnrollView: UdfpsEnrollView @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator @Mock private lateinit var featureFlags: FeatureFlags - @Mock private lateinit var mPrimaryBouncerInteractor: PrimaryBouncerInteractor + @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor + @Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams> private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true } @@ -138,10 +138,10 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { context, fingerprintManager, inflater, windowManager, accessibilityManager, statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager, keyguardUpdateMonitor, dialogManager, dumpManager, transitionController, - configurationController, systemClock, keyguardStateController, + configurationController, keyguardStateController, unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason, controllerCallback, onTouch, activityLaunchAnimator, featureFlags, - mPrimaryBouncerInteractor, isDebuggable + primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable, ) block() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index b061eb395119..0c34e54d2ec4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -81,6 +81,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -202,6 +203,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private PrimaryBouncerInteractor mPrimaryBouncerInteractor; @Mock private SinglePointerTouchProcessor mSinglePointerTouchProcessor; + @Mock + private AlternateBouncerInteractor mAlternateBouncerInteractor; // Capture listeners so that they can be used to send events @Captor @@ -292,7 +295,8 @@ public class UdfpsControllerTest extends SysuiTestCase { mDisplayManager, mHandler, mConfigurationController, mSystemClock, mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker, mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor, - mPrimaryBouncerInteractor, mSinglePointerTouchProcessor); + mPrimaryBouncerInteractor, mSinglePointerTouchProcessor, + mAlternateBouncerInteractor); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); @@ -406,7 +410,7 @@ public class UdfpsControllerTest extends SysuiTestCase { // GIVEN overlay was showing and the udfps bouncer is showing mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); - when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); // WHEN the overlay is hidden mOverlayController.hideUdfpsOverlay(mOpticalProps.sensorId); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java index 3c61382d9446..9c32c38e665c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java @@ -30,6 +30,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.ShadeExpansionChangeEvent; @@ -43,7 +44,6 @@ import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.DelayableExecutor; -import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.mockito.ArgumentCaptor; @@ -73,9 +73,9 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { protected @Mock ActivityLaunchAnimator mActivityLaunchAnimator; protected @Mock KeyguardBouncer mBouncer; protected @Mock PrimaryBouncerInteractor mPrimaryBouncerInteractor; + protected @Mock AlternateBouncerInteractor mAlternateBouncerInteractor; protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); - protected FakeSystemClock mSystemClock = new FakeSystemClock(); protected UdfpsKeyguardViewController mController; @@ -86,10 +86,6 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { private @Captor ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor; protected List<ShadeExpansionListener> mExpansionListeners; - private @Captor ArgumentCaptor<StatusBarKeyguardViewManager.AlternateBouncer> - mAlternateBouncerCaptor; - protected StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer; - private @Captor ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateControllerCallbackCaptor; protected KeyguardStateController.Callback mKeyguardStateControllerCallback; @@ -135,12 +131,6 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { } } - protected void captureAltAuthInterceptor() { - verify(mStatusBarKeyguardViewManager).setAlternateBouncer( - mAlternateBouncerCaptor.capture()); - mAlternateBouncer = mAlternateBouncerCaptor.getValue(); - } - protected void captureKeyguardStateControllerCallback() { verify(mKeyguardStateController).addCallback( mKeyguardStateControllerCallbackCaptor.capture()); @@ -160,6 +150,7 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { protected UdfpsKeyguardViewController createUdfpsKeyguardViewController( boolean useModernBouncer, boolean useExpandedOverlay) { mFeatureFlags.set(Flags.MODERN_BOUNCER, useModernBouncer); + mFeatureFlags.set(Flags.MODERN_ALTERNATE_BOUNCER, useModernBouncer); mFeatureFlags.set(Flags.UDFPS_NEW_TOUCH_DETECTION, useExpandedOverlay); when(mStatusBarKeyguardViewManager.getPrimaryBouncer()).thenReturn( useModernBouncer ? null : mBouncer); @@ -172,14 +163,14 @@ public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase { mDumpManager, mLockscreenShadeTransitionController, mConfigurationController, - mSystemClock, mKeyguardStateController, mUnlockedScreenOffAnimationController, mDialogManager, mUdfpsController, mActivityLaunchAnimator, mFeatureFlags, - mPrimaryBouncerInteractor); + mPrimaryBouncerInteractor, + mAlternateBouncerInteractor); return controller; } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java index babe5334e3eb..813eeeb557b8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java @@ -19,18 +19,15 @@ package com.android.systemui.biometrics; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper.RunWithLooper; +import android.testing.TestableLooper; import android.view.MotionEvent; import androidx.test.filters.SmallTest; @@ -46,7 +43,8 @@ import org.mockito.Captor; @SmallTest @RunWith(AndroidTestingRunner.class) -@RunWithLooper + +@TestableLooper.RunWithLooper(setAsMainLooper = true) public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewControllerBaseTest { private @Captor ArgumentCaptor<KeyguardBouncer.PrimaryBouncerExpansionCallback> mBouncerExpansionCallbackCaptor; @@ -72,8 +70,6 @@ public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewController assertTrue(mController.shouldPauseAuth()); } - - @Test public void testRegistersExpansionChangedListenerOnAttached() { mController.onViewAttached(); @@ -237,85 +233,9 @@ public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewController public void testOverrideShouldPauseAuthOnShadeLocked() { mController.onViewAttached(); captureStatusBarStateListeners(); - captureAltAuthInterceptor(); sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED); assertTrue(mController.shouldPauseAuth()); - - mAlternateBouncer.showAlternateBouncer(); // force show - assertFalse(mController.shouldPauseAuth()); - assertTrue(mAlternateBouncer.isShowingAlternateBouncer()); - - mAlternateBouncer.hideAlternateBouncer(); // stop force show - assertTrue(mController.shouldPauseAuth()); - assertFalse(mAlternateBouncer.isShowingAlternateBouncer()); - } - - @Test - public void testOnDetachedStateReset() { - // GIVEN view is attached - mController.onViewAttached(); - captureAltAuthInterceptor(); - - // WHEN view is detached - mController.onViewDetached(); - - // THEN remove alternate auth interceptor - verify(mStatusBarKeyguardViewManager).removeAlternateAuthInterceptor(mAlternateBouncer); - } - - @Test - public void testHiddenUdfpsBouncerOnTouchOutside_nothingHappens() { - // GIVEN view is attached - mController.onViewAttached(); - captureAltAuthInterceptor(); - - // GIVEN udfps bouncer isn't showing - mAlternateBouncer.hideAlternateBouncer(); - - // WHEN touch is observed outside the view - mController.onTouchOutsideView(); - - // THEN bouncer / alt auth methods are never called - verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); - verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean()); - verify(mStatusBarKeyguardViewManager, never()).hideAlternateBouncer(anyBoolean()); - } - - @Test - public void testShowingUdfpsBouncerOnTouchOutsideWithinThreshold_nothingHappens() { - // GIVEN view is attached - mController.onViewAttached(); - captureAltAuthInterceptor(); - - // GIVEN udfps bouncer is showing - mAlternateBouncer.showAlternateBouncer(); - - // WHEN touch is observed outside the view 200ms later (just within threshold) - mSystemClock.advanceTime(200); - mController.onTouchOutsideView(); - - // THEN bouncer / alt auth methods are never called because not enough time has passed - verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); - verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean()); - verify(mStatusBarKeyguardViewManager, never()).hideAlternateBouncer(anyBoolean()); - } - - @Test - public void testShowingUdfpsBouncerOnTouchOutsideAboveThreshold_showPrimaryBouncer() { - // GIVEN view is attached - mController.onViewAttached(); - captureAltAuthInterceptor(); - - // GIVEN udfps bouncer is showing - mAlternateBouncer.showAlternateBouncer(); - - // WHEN touch is observed outside the view 205ms later - mSystemClock.advanceTime(205); - mController.onTouchOutsideView(); - - // THEN show the bouncer - verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(eq(true)); } @Test @@ -334,25 +254,6 @@ public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewController } @Test - public void testShowUdfpsBouncer() { - // GIVEN view is attached and status bar expansion is 0 - mController.onViewAttached(); - captureStatusBarExpansionListeners(); - captureKeyguardStateControllerCallback(); - captureAltAuthInterceptor(); - updateStatusBarExpansion(0, true); - reset(mView); - when(mView.getContext()).thenReturn(mResourceContext); - when(mResourceContext.getString(anyInt())).thenReturn("test string"); - - // WHEN status bar expansion is 0 but udfps bouncer is requested - mAlternateBouncer.showAlternateBouncer(); - - // THEN alpha is 255 - verify(mView).setUnpausedAlpha(255); - } - - @Test public void testTransitionToFullShadeProgress() { // GIVEN view is attached and status bar expansion is 1f mController.onViewAttached(); @@ -370,24 +271,6 @@ public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewController } @Test - public void testShowUdfpsBouncer_transitionToFullShadeProgress() { - // GIVEN view is attached and status bar expansion is 1f - mController.onViewAttached(); - captureStatusBarExpansionListeners(); - captureKeyguardStateControllerCallback(); - captureAltAuthInterceptor(); - updateStatusBarExpansion(1f, true); - mAlternateBouncer.showAlternateBouncer(); - reset(mView); - - // WHEN we're transitioning to the full shade - mController.setTransitionToFullShadeProgress(1.0f); - - // THEN alpha is 255 (b/c udfps bouncer is requested) - verify(mView).setUnpausedAlpha(255); - } - - @Test public void testUpdatePanelExpansion_pauseAuth() { // GIVEN view is attached + on the keyguard mController.onViewAttached(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt index 2d412dcaa909..3b4f7e10c806 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt @@ -21,26 +21,35 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardSecurityModel +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.BouncerView +import com.android.systemui.keyguard.data.repository.BiometricRepository import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.KeyguardBouncer import com.android.systemui.statusbar.phone.KeyguardBypassController +import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.time.SystemClock import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.yield +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any import org.mockito.Mock import org.mockito.Mockito.mock +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -57,6 +66,7 @@ class UdfpsKeyguardViewControllerWithCoroutinesTest : UdfpsKeyguardViewControlle keyguardBouncerRepository = KeyguardBouncerRepository( mock(com.android.keyguard.ViewMediatorCallback::class.java), + FakeSystemClock(), TestCoroutineScope(), bouncerLogger, ) @@ -77,15 +87,43 @@ class UdfpsKeyguardViewControllerWithCoroutinesTest : UdfpsKeyguardViewControlle mock(KeyguardBypassController::class.java), mKeyguardUpdateMonitor ) + mAlternateBouncerInteractor = + AlternateBouncerInteractor( + keyguardBouncerRepository, + mock(BiometricRepository::class.java), + mock(SystemClock::class.java), + mock(KeyguardUpdateMonitor::class.java), + mock(FeatureFlags::class.java) + ) return createUdfpsKeyguardViewController( /* useModernBouncer */ true, /* useExpandedOverlay */ false ) } - /** After migration, replaces LockIconViewControllerTest version */ @Test - fun testShouldPauseAuthBouncerShowing() = + fun shadeLocked_showAlternateBouncer_unpauseAuth() = + runBlocking(IMMEDIATE) { + // GIVEN view is attached + on the SHADE_LOCKED (udfps view not showing) + mController.onViewAttached() + captureStatusBarStateListeners() + sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED) + + // WHEN alternate bouncer is requested + val job = mController.listenForAlternateBouncerVisibility(this) + keyguardBouncerRepository.setAlternateVisible(true) + yield() + + // THEN udfps view will animate in & pause auth is updated to NOT pause + verify(mView).animateInUdfpsBouncer(any()) + assertFalse(mController.shouldPauseAuth()) + + job.cancel() + } + + /** After migration to MODERN_BOUNCER, replaces UdfpsKeyguardViewControllerTest version */ + @Test + fun shouldPauseAuthBouncerShowing() = runBlocking(IMMEDIATE) { // GIVEN view attached and we're on the keyguard mController.onViewAttached() 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 d550b927154c..8255a1452bd1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt @@ -80,15 +80,6 @@ class UdfpsViewTest : SysuiTestCase() { } @Test - fun forwardsEvents() { - view.dozeTimeTick() - verify(animationViewController).dozeTimeTick() - - view.onTouchOutsideView() - verify(animationViewController).onTouchOutsideView() - } - - @Test fun layoutSizeFitsSensor() { val params = withArgCaptor<RectF> { verify(animationViewController).onSensorRectUpdated(capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt new file mode 100644 index 000000000000..af46d9b97abf --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/EllipseOverlapDetectorTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.udfps + +import android.graphics.Point +import android.graphics.Rect +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters +import org.mockito.Mockito.spy +import org.mockito.Mockito.`when` as whenEver + +@SmallTest +@RunWith(Parameterized::class) +class EllipseOverlapDetectorTest(val testCase: TestCase) : SysuiTestCase() { + val underTest = spy(EllipseOverlapDetector(neededPoints = 1)) + + @Before + fun setUp() { + // Use one single center point for testing, required or total number of points may change + whenEver(underTest.calculateSensorPoints(SENSOR)) + .thenReturn(listOf(Point(SENSOR.centerX(), SENSOR.centerY()))) + } + + @Test + fun isGoodOverlap() { + val touchData = + TOUCH_DATA.copy( + x = testCase.x.toFloat(), + y = testCase.y.toFloat(), + minor = testCase.minor, + major = testCase.major + ) + val actual = underTest.isGoodOverlap(touchData, SENSOR) + + assertThat(actual).isEqualTo(testCase.expected) + } + + data class TestCase( + val x: Int, + val y: Int, + val minor: Float, + val major: Float, + val expected: Boolean + ) + + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun data(): List<TestCase> = + listOf( + genTestCases( + innerXs = listOf(SENSOR.left, SENSOR.right, SENSOR.centerX()), + innerYs = listOf(SENSOR.top, SENSOR.bottom, SENSOR.centerY()), + outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1), + outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + minor = 300f, + major = 300f, + expected = true + ), + genTestCases( + innerXs = listOf(SENSOR.left, SENSOR.right), + innerYs = listOf(SENSOR.top, SENSOR.bottom), + outerXs = listOf(SENSOR.left - 1, SENSOR.right + 1), + outerYs = listOf(SENSOR.top - 1, SENSOR.bottom + 1), + minor = 100f, + major = 100f, + expected = false + ) + ) + .flatten() + } +} + +/* Placeholder touch parameters. */ +private const val POINTER_ID = 42 +private const val NATIVE_MINOR = 2.71828f +private const val NATIVE_MAJOR = 3.14f +private const val ORIENTATION = 0f // used for perfect circles +private const val TIME = 12345699L +private const val GESTURE_START = 12345600L + +/* Template [NormalizedTouchData]. */ +private val TOUCH_DATA = + NormalizedTouchData( + POINTER_ID, + x = 0f, + y = 0f, + NATIVE_MINOR, + NATIVE_MAJOR, + ORIENTATION, + TIME, + GESTURE_START + ) + +private val SENSOR = Rect(100 /* left */, 200 /* top */, 300 /* right */, 400 /* bottom */) + +private fun genTestCases( + innerXs: List<Int>, + innerYs: List<Int>, + outerXs: List<Int>, + outerYs: List<Int>, + minor: Float, + major: Float, + expected: Boolean +): List<EllipseOverlapDetectorTest.TestCase> { + return (innerXs + outerXs).flatMap { x -> + (innerYs + outerYs).map { y -> + EllipseOverlapDetectorTest.TestCase(x, y, minor, major, expected) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt index 95c53b408056..56043e306c16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt @@ -221,6 +221,14 @@ class SinglePointerTouchProcessorTest(val testCase: TestCase) : SysuiTestCase() private const val ROTATION_0_NATIVE_DISPLAY_WIDTH = 400 private const val ROTATION_0_NATIVE_DISPLAY_HEIGHT = 600 +/* Placeholder touch parameters. */ +private const val POINTER_ID = 42 +private const val NATIVE_MINOR = 2.71828f +private const val NATIVE_MAJOR = 3.14f +private const val ORIENTATION = 1.2345f +private const val TIME = 12345699L +private const val GESTURE_START = 12345600L + /* * ROTATION_0 map: * _ _ _ _ @@ -244,6 +252,7 @@ private val ROTATION_0_NATIVE_SENSOR_BOUNDS = private val ROTATION_0_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_0, + nativeOrientation = ORIENTATION, nativeXWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 250f, @@ -271,6 +280,7 @@ private val ROTATION_90_NATIVE_SENSOR_BOUNDS = private val ROTATION_90_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_90, + nativeOrientation = (ORIENTATION - Math.PI.toFloat() / 2), nativeXWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 150f, @@ -304,20 +314,13 @@ private val ROTATION_270_NATIVE_SENSOR_BOUNDS = private val ROTATION_270_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_270, + nativeOrientation = (ORIENTATION + Math.PI.toFloat() / 2), nativeXWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 450f, nativeYOutsideSensor = 250f, ) -/* Placeholder touch parameters. */ -private const val POINTER_ID = 42 -private const val NATIVE_MINOR = 2.71828f -private const val NATIVE_MAJOR = 3.14f -private const val ORIENTATION = 1.23f -private const val TIME = 12345699L -private const val GESTURE_START = 12345600L - /* Template [MotionEvent]. */ private val MOTION_EVENT = obtainMotionEvent( @@ -352,6 +355,7 @@ private val NORMALIZED_TOUCH_DATA = */ private data class OrientationBasedInputs( @Rotation val rotation: Int, + val nativeOrientation: Float, val nativeXWithinSensor: Float, val nativeYWithinSensor: Float, val nativeXOutsideSensor: Float, @@ -404,6 +408,7 @@ private fun genPositiveTestCases( y = nativeY * scaleFactor, minor = NATIVE_MINOR * scaleFactor, major = NATIVE_MAJOR * scaleFactor, + orientation = orientation.nativeOrientation ) val expectedTouchData = NORMALIZED_TOUCH_DATA.copy( diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java index 0fadc138637a..e4df754ec96a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java @@ -106,6 +106,7 @@ public class BrightLineClassifierTest extends SysuiTestCase { mClassifiers.add(mClassifierB); when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList); when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mFalsingDataProvider.isFolded()).thenReturn(true); mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, mMetricsLogger, mClassifiers, mSingleTapClassfier, mLongTapClassifier, mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController, @@ -121,6 +122,7 @@ public class BrightLineClassifierTest extends SysuiTestCase { mGestureFinalizedListener = gestureCompleteListenerCaptor.getValue(); mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true); mFakeFeatureFlags.set(Flags.MEDIA_FALSING_PENALTY, true); + mFakeFeatureFlags.set(Flags.FALSING_OFF_FOR_UNFOLDED, true); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java index 4281ee0f139f..ae38eb67c431 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java @@ -89,25 +89,27 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { mClassifiers.add(mClassifierA); when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList); when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mFalsingDataProvider.isFolded()).thenReturn(true); mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, mMetricsLogger, mClassifiers, mSingleTapClassifier, mLongTapClassifier, mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController, mAccessibilityManager, false, mFakeFeatureFlags); mFakeFeatureFlags.set(Flags.FALSING_FOR_LONG_TAPS, true); + mFakeFeatureFlags.set(Flags.FALSING_OFF_FOR_UNFOLDED, true); } @Test public void testA11yDisablesGesture() { - assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue(); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true); - assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse(); } @Test public void testA11yDisablesTap() { - assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); + assertThat(mBrightLineFalsingManager.isFalseTap(1)).isTrue(); when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(true); - assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse(); + assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); } @@ -179,4 +181,11 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { when(mFalsingDataProvider.isA11yAction()).thenReturn(true); assertThat(mBrightLineFalsingManager.isFalseTap(1)).isFalse(); } + + @Test + public void testSkipUnfolded() { + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); + when(mFalsingDataProvider.isFolded()).thenReturn(false); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java index 5fa7214f07ff..94cf384267ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/ClassifierTest.java @@ -16,6 +16,7 @@ package com.android.systemui.classifier; +import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.util.DisplayMetrics; import android.view.MotionEvent; @@ -38,6 +39,7 @@ public class ClassifierTest extends SysuiTestCase { private float mOffsetY = 0; @Mock private BatteryController mBatteryController; + private FoldStateListener mFoldStateListener = new FoldStateListener(mContext); private final DockManagerFake mDockManager = new DockManagerFake(); public void setup() { @@ -47,7 +49,8 @@ public class ClassifierTest extends SysuiTestCase { displayMetrics.ydpi = 100; displayMetrics.widthPixels = 1000; displayMetrics.heightPixels = 1000; - mDataProvider = new FalsingDataProvider(displayMetrics, mBatteryController, mDockManager); + mDataProvider = new FalsingDataProvider( + displayMetrics, mBatteryController, mFoldStateListener, mDockManager); } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java index d315c2da0703..c451a1e754c9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; import android.view.MotionEvent; @@ -50,6 +51,8 @@ public class FalsingDataProviderTest extends ClassifierTest { private FalsingDataProvider mDataProvider; @Mock private BatteryController mBatteryController; + @Mock + private FoldStateListener mFoldStateListener; private final DockManagerFake mDockManager = new DockManagerFake(); @Before @@ -61,7 +64,8 @@ public class FalsingDataProviderTest extends ClassifierTest { displayMetrics.ydpi = 100; displayMetrics.widthPixels = 1000; displayMetrics.heightPixels = 1000; - mDataProvider = new FalsingDataProvider(displayMetrics, mBatteryController, mDockManager); + mDataProvider = new FalsingDataProvider( + displayMetrics, mBatteryController, mFoldStateListener, mDockManager); } @After @@ -316,4 +320,16 @@ public class FalsingDataProviderTest extends ClassifierTest { mDataProvider.onA11yAction(); assertThat(mDataProvider.isA11yAction()).isTrue(); } + + @Test + public void test_FoldedState_Folded() { + when(mFoldStateListener.getFolded()).thenReturn(true); + assertThat(mDataProvider.isFolded()).isTrue(); + } + + @Test + public void test_FoldedState_Unfolded() { + when(mFoldStateListener.getFolded()).thenReturn(false); + assertThat(mDataProvider.isFolded()).isFalse(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt index 0a81c38e7448..ebbe096b0da3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt @@ -269,6 +269,14 @@ class ControlsBindingControllerImplTest : SysuiTestCase() { } @Test + fun testBindServiceForPanel() { + controller.bindServiceForPanel(TEST_COMPONENT_NAME_1) + executor.runAllReady() + + verify(providers[0]).bindServiceForPanel() + } + + @Test fun testSubscribe() { val controlInfo1 = ControlInfo("id_1", "", "", DeviceTypes.TYPE_UNKNOWN) val controlInfo2 = ControlInfo("id_2", "", "", DeviceTypes.TYPE_UNKNOWN) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt index 1b34706bd220..25f471b0d3e0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt @@ -919,6 +919,12 @@ class ControlsControllerImplTest : SysuiTestCase() { .getFile(ControlsFavoritePersistenceWrapper.FILE_NAME, context.user.identifier) assertThat(userStructure.file).isNotNull() } + + @Test + fun testBindForPanel() { + controller.bindComponentForPanel(TEST_COMPONENT) + verify(bindingController).bindServiceForPanel(TEST_COMPONENT) + } } private class DidRunRunnable() : Runnable { diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt index af3f24a1c58a..da548f7ccef2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt @@ -105,6 +105,22 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() { } @Test + fun testBindForPanel() { + manager.bindServiceForPanel() + executor.runAllReady() + assertTrue(context.isBound(componentName)) + } + + @Test + fun testUnbindPanelIsUnbound() { + manager.bindServiceForPanel() + executor.runAllReady() + manager.unbindService() + executor.runAllReady() + assertFalse(context.isBound(componentName)) + } + + @Test fun testNullBinding() { val mockContext = mock(Context::class.java) lateinit var serviceConnection: ServiceConnection diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index d172c9a2e630..edc6882e71c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -229,6 +229,15 @@ class ControlsUiControllerImplTest : SysuiTestCase() { } @Test + fun testPanelBindsForPanel() { + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + setUpPanel(panel) + + underTest.show(parent, {}, context) + verify(controlsController).bindComponentForPanel(panel.componentName) + } + + @Test fun testPanelCallsTaskViewFactoryCreate() { mockLayoutInflater() val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 122d7fdbfa47..f55b86686152 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -29,6 +29,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -485,6 +486,38 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { assertTrue(mViewMediator.isShowingAndNotOccluded()); } + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testDoKeyguardWhileInteractive_resets() { + mViewMediator.setShowingLocked(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + TestableLooper.get(this).processAllMessages(); + + when(mPowerManager.isInteractive()).thenReturn(true); + + mViewMediator.onSystemReady(); + TestableLooper.get(this).processAllMessages(); + + assertTrue(mViewMediator.isShowingAndNotOccluded()); + verify(mStatusBarKeyguardViewManager).reset(anyBoolean()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testDoKeyguardWhileNotInteractive_showsInsteadOfResetting() { + mViewMediator.setShowingLocked(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + TestableLooper.get(this).processAllMessages(); + + when(mPowerManager.isInteractive()).thenReturn(false); + + mViewMediator.onSystemReady(); + TestableLooper.get(this).processAllMessages(); + + assertTrue(mViewMediator.isShowingAndNotOccluded()); + verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean()); + } + private void createAndStartViewMediator() { mViewMediator = new KeyguardViewMediator( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java index f32d76bb601e..39a453da7f92 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java @@ -30,6 +30,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -51,7 +52,12 @@ public class WakefulnessLifecycleTest extends SysuiTestCase { public void setUp() throws Exception { mWallpaperManager = mock(IWallpaperManager.class); mWakefulness = - new WakefulnessLifecycle(mContext, mWallpaperManager, mock(DumpManager.class)); + new WakefulnessLifecycle( + mContext, + mWallpaperManager, + new FakeSystemClock(), + mock(DumpManager.class) + ); mWakefulnessObserver = mock(WakefulnessLifecycle.Observer.class); mWakefulness.addObserver(mWakefulnessObserver); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt new file mode 100644 index 000000000000..a92dd3b92397 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricRepositoryTest.kt @@ -0,0 +1,176 @@ +/* + * 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.keyguard.data.repository + +import android.app.admin.DevicePolicyManager +import android.content.Intent +import android.content.pm.UserInfo +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED +import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.AuthController +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +class BiometricRepositoryTest : SysuiTestCase() { + private lateinit var underTest: BiometricRepository + + @Mock private lateinit var authController: AuthController + @Mock private lateinit var lockPatternUtils: LockPatternUtils + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + private lateinit var userRepository: FakeUserRepository + + private lateinit var testDispatcher: TestDispatcher + private lateinit var testScope: TestScope + private var testableLooper: TestableLooper? = null + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testableLooper = TestableLooper.get(this) + testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + userRepository = FakeUserRepository() + } + + private suspend fun createBiometricRepository() { + userRepository.setUserInfos(listOf(PRIMARY_USER)) + userRepository.setSelectedUserInfo(PRIMARY_USER) + underTest = + BiometricRepositoryImpl( + context = context, + lockPatternUtils = lockPatternUtils, + broadcastDispatcher = fakeBroadcastDispatcher, + authController = authController, + userRepository = userRepository, + devicePolicyManager = devicePolicyManager, + scope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, + looper = testableLooper!!.looper, + ) + } + + @Test + fun fingerprintEnrollmentChange() = + testScope.runTest { + createBiometricRepository() + val fingerprintEnabledByDevicePolicy = collectLastValue(underTest.isFingerprintEnrolled) + runCurrent() + + val captor = argumentCaptor<AuthController.Callback>() + verify(authController).addCallback(captor.capture()) + whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(true) + captor.value.onEnrollmentsChanged( + BiometricType.UNDER_DISPLAY_FINGERPRINT, + PRIMARY_USER_ID, + true + ) + assertThat(fingerprintEnabledByDevicePolicy()).isTrue() + + whenever(authController.isFingerprintEnrolled(anyInt())).thenReturn(false) + captor.value.onEnrollmentsChanged( + BiometricType.UNDER_DISPLAY_FINGERPRINT, + PRIMARY_USER_ID, + false + ) + assertThat(fingerprintEnabledByDevicePolicy()).isFalse() + } + + @Test + fun strongBiometricAllowedChange() = + testScope.runTest { + createBiometricRepository() + val strongBiometricAllowed = collectLastValue(underTest.isStrongBiometricAllowed) + runCurrent() + + val captor = argumentCaptor<LockPatternUtils.StrongAuthTracker>() + verify(lockPatternUtils).registerStrongAuthTracker(captor.capture()) + + captor.value + .getStub() + .onStrongAuthRequiredChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) + testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + assertThat(strongBiometricAllowed()).isTrue() + + captor.value + .getStub() + .onStrongAuthRequiredChanged(STRONG_AUTH_REQUIRED_AFTER_BOOT, PRIMARY_USER_ID) + testableLooper?.processAllMessages() // StrongAuthTracker uses the TestableLooper + assertThat(strongBiometricAllowed()).isFalse() + } + + @Test + fun fingerprintDisabledByDpmChange() = + testScope.runTest { + createBiometricRepository() + val fingerprintEnabledByDevicePolicy = + collectLastValue(underTest.isFingerprintEnabledByDevicePolicy) + runCurrent() + + whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())) + .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT) + broadcastDPMStateChange() + assertThat(fingerprintEnabledByDevicePolicy()).isFalse() + + whenever(devicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())).thenReturn(0) + broadcastDPMStateChange() + assertThat(fingerprintEnabledByDevicePolicy()).isTrue() + } + + private fun broadcastDPMStateChange() { + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive( + context, + Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED) + ) + } + } + + companion object { + private const val PRIMARY_USER_ID = 0 + private val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt index 9970a6796ed6..969537d23111 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt @@ -20,6 +20,7 @@ import androidx.test.filters.SmallTest import com.android.keyguard.ViewMediatorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.util.time.SystemClock import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope import org.junit.Before @@ -34,6 +35,7 @@ import org.mockito.MockitoAnnotations @RunWith(JUnit4::class) class KeyguardBouncerRepositoryTest : SysuiTestCase() { + @Mock private lateinit var systemClock: SystemClock @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback @Mock private lateinit var bouncerLogger: TableLogBuffer lateinit var underTest: KeyguardBouncerRepository @@ -43,7 +45,12 @@ class KeyguardBouncerRepositoryTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) val testCoroutineScope = TestCoroutineScope() underTest = - KeyguardBouncerRepository(viewMediatorCallback, testCoroutineScope, bouncerLogger) + KeyguardBouncerRepository( + viewMediatorCallback, + systemClock, + testCoroutineScope, + bouncerLogger, + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index be712f699b7b..f997d18a57a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -24,6 +24,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController import com.android.systemui.common.shared.model.Position +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback @@ -38,14 +39,17 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController +import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -68,6 +72,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var authController: AuthController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController + @Mock private lateinit var dozeParameters: DozeParameters private lateinit var underTest: KeyguardRepositoryImpl @@ -84,6 +89,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { keyguardStateController, keyguardUpdateMonitor, dozeTransitionListener, + dozeParameters, authController, dreamOverlayCallbackController, ) @@ -170,6 +176,26 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun isAodAvailable() = runTest { + val flow = underTest.isAodAvailable + var isAodAvailable = collectLastValue(flow) + runCurrent() + + val callback = + withArgCaptor<DozeParameters.Callback> { verify(dozeParameters).addCallback(capture()) } + + whenever(dozeParameters.getAlwaysOn()).thenReturn(false) + callback.onAlwaysOnChange() + assertThat(isAodAvailable()).isEqualTo(false) + + whenever(dozeParameters.getAlwaysOn()).thenReturn(true) + callback.onAlwaysOnChange() + assertThat(isAodAvailable()).isEqualTo(true) + + flow.onCompletion { verify(dozeParameters).removeCallback(callback) } + } + + @Test fun isKeyguardOccluded() = runTest(UnconfinedTestDispatcher()) { whenever(keyguardStateController.isOccluded).thenReturn(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt index f8f2a56d4808..32cec09c3580 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -168,6 +168,25 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { assertThat(wtfHandler.failed).isTrue() } + @Test + fun `Attempt to manually update transition after CANCELED state throws exception`() { + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 0.2f, TransitionState.CANCELED) + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + } + assertThat(wtfHandler.failed).isTrue() + } + private fun listWithStep( step: BigDecimal, start: BigDecimal = BigDecimal.ZERO, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt new file mode 100644 index 000000000000..1da7241e58bd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt @@ -0,0 +1,156 @@ +/* + * 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.keyguard.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.ViewMediatorCallback +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.FakeBiometricRepository +import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.time.SystemClock +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScope +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class AlternateBouncerInteractorTest : SysuiTestCase() { + private lateinit var underTest: AlternateBouncerInteractor + private lateinit var bouncerRepository: KeyguardBouncerRepository + private lateinit var biometricRepository: FakeBiometricRepository + @Mock private lateinit var systemClock: SystemClock + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var bouncerLogger: TableLogBuffer + private lateinit var featureFlags: FakeFeatureFlags + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + bouncerRepository = + KeyguardBouncerRepository( + mock(ViewMediatorCallback::class.java), + FakeSystemClock(), + TestCoroutineScope(), + bouncerLogger, + ) + biometricRepository = FakeBiometricRepository() + featureFlags = FakeFeatureFlags().apply { this.set(Flags.MODERN_ALTERNATE_BOUNCER, true) } + underTest = + AlternateBouncerInteractor( + bouncerRepository, + biometricRepository, + systemClock, + keyguardUpdateMonitor, + featureFlags, + ) + } + + @Test + fun canShowAlternateBouncerForFingerprint_givenCanShow() { + givenCanShowAlternateBouncer() + assertTrue(underTest.canShowAlternateBouncerForFingerprint()) + } + + @Test + fun canShowAlternateBouncerForFingerprint_alternateBouncerUIUnavailable() { + givenCanShowAlternateBouncer() + bouncerRepository.setAlternateBouncerUIAvailable(false) + + assertFalse(underTest.canShowAlternateBouncerForFingerprint()) + } + + @Test + fun canShowAlternateBouncerForFingerprint_noFingerprintsEnrolled() { + givenCanShowAlternateBouncer() + biometricRepository.setFingerprintEnrolled(false) + + assertFalse(underTest.canShowAlternateBouncerForFingerprint()) + } + + @Test + fun canShowAlternateBouncerForFingerprint_strongBiometricNotAllowed() { + givenCanShowAlternateBouncer() + biometricRepository.setStrongBiometricAllowed(false) + + assertFalse(underTest.canShowAlternateBouncerForFingerprint()) + } + + @Test + fun canShowAlternateBouncerForFingerprint_devicePolicyDoesNotAllowFingerprint() { + givenCanShowAlternateBouncer() + biometricRepository.setFingerprintEnabledByDevicePolicy(false) + + assertFalse(underTest.canShowAlternateBouncerForFingerprint()) + } + + @Test + fun show_whenCanShow() { + givenCanShowAlternateBouncer() + + assertTrue(underTest.show()) + assertTrue(bouncerRepository.isAlternateBouncerVisible.value) + } + + @Test + fun show_whenCannotShow() { + givenCannotShowAlternateBouncer() + + assertFalse(underTest.show()) + assertFalse(bouncerRepository.isAlternateBouncerVisible.value) + } + + @Test + fun hide_wasPreviouslyShowing() { + bouncerRepository.setAlternateVisible(true) + + assertTrue(underTest.hide()) + assertFalse(bouncerRepository.isAlternateBouncerVisible.value) + } + + @Test + fun hide_wasNotPreviouslyShowing() { + bouncerRepository.setAlternateVisible(false) + + assertFalse(underTest.hide()) + assertFalse(bouncerRepository.isAlternateBouncerVisible.value) + } + + private fun givenCanShowAlternateBouncer() { + bouncerRepository.setAlternateBouncerUIAvailable(true) + biometricRepository.setFingerprintEnrolled(true) + biometricRepository.setStrongBiometricAllowed(true) + biometricRepository.setFingerprintEnabledByDevicePolicy(true) + } + + private fun givenCannotShowAlternateBouncer() { + biometricRepository.setFingerprintEnrolled(false) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 754adfdc48b3..a1b6d478d799 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -71,6 +72,10 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor + private lateinit var fromDozingTransitionInteractor: FromDozingTransitionInteractor + private lateinit var fromOccludedTransitionInteractor: FromOccludedTransitionInteractor + private lateinit var fromGoneTransitionInteractor: FromGoneTransitionInteractor + private lateinit var fromAodTransitionInteractor: FromAodTransitionInteractor @Before fun setUp() { @@ -102,6 +107,42 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), ) fromDreamingTransitionInteractor.start() + + fromAodTransitionInteractor = + FromAodTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromAodTransitionInteractor.start() + + fromGoneTransitionInteractor = + FromGoneTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromGoneTransitionInteractor.start() + + fromDozingTransitionInteractor = + FromDozingTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromDozingTransitionInteractor.start() + + fromOccludedTransitionInteractor = + FromOccludedTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromOccludedTransitionInteractor.start() } @Test @@ -137,7 +178,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardRepository.setDreamingWithOverlay(false) // AND occluded has stopped keyguardRepository.setKeyguardOccluded(false) - runCurrent() + advanceUntilIdle() val info = withArgCaptor<TransitionInfo> { @@ -192,6 +233,332 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { coroutineContext.cancelChildren() } + @Test + fun `OCCLUDED to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `OCCLUDED to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `LOCKSCREEN to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to LOCKSCREEN + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `LOCKSCREEN to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to LOCKSCREEN + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `DOZING to LOCKSCREEN`() = + testScope.runTest { + // GIVEN a prior transition has run to DOZING + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DOZING, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to wake + keyguardRepository.setWakefulnessModel(startingToWake()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.DOZING) + assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `GONE to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to GONE + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `GONE to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to GONE + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to AOD should occur + assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `GONE to DREAMING`() = + testScope.runTest { + // GIVEN a device that is not dreaming or dozing + keyguardRepository.setDreamingWithOverlay(false) + keyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) + ) + runCurrent() + + // GIVEN a prior transition has run to GONE + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + reset(mockTransitionRepository) + + // WHEN the device begins to dream + keyguardRepository.setDreamingWithOverlay(true) + advanceUntilIdle() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DREAMING should occur + assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.DREAMING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + private fun startingToWake() = WakefulnessModel( WakefulnessState.STARTING_TO_WAKE, @@ -199,4 +566,12 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { WakeSleepReason.OTHER, WakeSleepReason.OTHER ) + + private fun startingToSleep() = + WakefulnessModel( + WakefulnessState.STARTING_TO_SLEEP, + true, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt new file mode 100644 index 000000000000..7fa204bb980b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA +import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class GoneToDreamingTransitionViewModelTest : SysuiTestCase() { + private lateinit var underTest: GoneToDreamingTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + repository = FakeKeyguardTransitionRepository() + val interactor = KeyguardTransitionInteractor(repository) + underTest = GoneToDreamingTransitionViewModel(interactor) + } + + @Test + fun lockscreenFadeOut() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) + + // Should start running here... + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.1f)) + repository.sendTransitionStep(step(0.2f)) + // ...up to here + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(1f)) + + // Only three values should be present, since the dream overlay runs for a small + // fraction + // of the overall animation time + assertThat(values.size).isEqualTo(3) + assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA)) + assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA)) + assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA)) + + job.cancel() + } + + @Test + fun lockscreenTranslationY() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val pixels = 100 + val job = + underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) + + // Should start running here... + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.5f)) + // ...up to here + repository.sendTransitionStep(step(1f)) + // And a final reset event on CANCEL + repository.sendTransitionStep(step(0.8f, TransitionState.CANCELED)) + + assertThat(values.size).isEqualTo(4) + assertThat(values[0]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[1]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[2]) + .isEqualTo( + EMPHASIZED_ACCELERATE.getInterpolation( + animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[3]).isEqualTo(0f) + job.cancel() + } + + private fun animValue(stepValue: Float, params: AnimationParams): Float { + val totalDuration = TO_DREAMING_DURATION + val startValue = (params.startTime / totalDuration).toFloat() + + val multiplier = (totalDuration / params.duration).toFloat() + return (stepValue - startValue) * multiplier + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.DREAMING, + value = value, + transitionState = state, + ownerName = "GoneToDreamingTransitionViewModelTest" + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt index 739059126b04..539fc2c1548e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt @@ -67,8 +67,7 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { repository.sendTransitionStep(step(1f)) // Only three values should be present, since the dream overlay runs for a small - // fraction - // of the overall animation time + // fraction of the overall animation time assertThat(values.size).isEqualTo(3) assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA)) assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA)) @@ -92,8 +91,10 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { repository.sendTransitionStep(step(0.5f)) // ...up to here repository.sendTransitionStep(step(1f)) + // And a final reset event on FINISHED + repository.sendTransitionStep(step(1f, TransitionState.FINISHED)) - assertThat(values.size).isEqualTo(3) + assertThat(values.size).isEqualTo(4) assertThat(values[0]) .isEqualTo( EMPHASIZED_ACCELERATE.getInterpolation( @@ -112,6 +113,8 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) ) * pixels ) + assertThat(values[3]).isEqualTo(0f) + job.cancel() } @@ -123,12 +126,15 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { return (stepValue - startValue) * multiplier } - private fun step(value: Float): TransitionStep { + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { return TransitionStep( from = KeyguardState.LOCKSCREEN, to = KeyguardState.DREAMING, value = value, - transitionState = TransitionState.RUNNING, + transitionState = state, ownerName = "LockscreenToDreamingTransitionViewModelTest" ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt new file mode 100644 index 000000000000..411b1bd04c52 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.table + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class TableLogBufferFactoryTest : SysuiTestCase() { + private val dumpManager: DumpManager = mock() + private val systemClock = FakeSystemClock() + private val underTest = TableLogBufferFactory(dumpManager, systemClock) + + @Test + fun `create - always creates new instance`() { + val b1 = underTest.create(NAME_1, SIZE) + val b1_copy = underTest.create(NAME_1, SIZE) + val b2 = underTest.create(NAME_2, SIZE) + val b2_copy = underTest.create(NAME_2, SIZE) + + assertThat(b1).isNotSameInstanceAs(b1_copy) + assertThat(b1).isNotSameInstanceAs(b2) + assertThat(b2).isNotSameInstanceAs(b2_copy) + } + + @Test + fun `getOrCreate - reuses instance`() { + val b1 = underTest.getOrCreate(NAME_1, SIZE) + val b1_copy = underTest.getOrCreate(NAME_1, SIZE) + val b2 = underTest.getOrCreate(NAME_2, SIZE) + val b2_copy = underTest.getOrCreate(NAME_2, SIZE) + + assertThat(b1).isSameInstanceAs(b1_copy) + assertThat(b2).isSameInstanceAs(b2_copy) + assertThat(b1).isNotSameInstanceAs(b2) + assertThat(b1_copy).isNotSameInstanceAs(b2_copy) + } + + companion object { + const val NAME_1 = "name 1" + const val NAME_2 = "name 2" + + const val SIZE = 8 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java index 4d2d0f05b76a..c0639f34484c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java @@ -79,7 +79,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { USER_ID, true, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null, MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, - InstanceId.fakeInstanceId(-1), -1); + InstanceId.fakeInstanceId(-1), -1, false); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index 52b694fac07c..c24c8c7f7cf6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -228,6 +228,7 @@ class MediaDataManagerTest : SysuiTestCase() { whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L) whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false) + whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) } @@ -300,6 +301,60 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testLoadMetadata_withExplicitIndicator() { + val metadata = + MediaMetadata.Builder().run { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + build() + } + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + whenever(controller.metadata).thenReturn(metadata) + + mediaDataManager.addListener(listener) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isTrue() + } + + @Test + fun testOnMetaDataLoaded_withoutExplicitIndicator() { + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + + mediaDataManager.addListener(listener) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isFalse() + } + + @Test fun testOnMetaDataLoaded_callsListener() { addNotificationAndLoad() verify(logger) @@ -603,6 +658,53 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testAddResumptionControls_withExplicitIndicator() { + val bundle = Bundle() + // WHEN resumption controls are added with explicit indicator + bundle.putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(bundle) + build() + } + val currentTime = clock.elapsedRealtime() + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + // THEN the media data indicates that it is for resumption + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.song).isEqualTo(SESSION_TITLE) + assertThat(data.app).isEqualTo(APP_NAME) + assertThat(data.actions).hasSize(1) + assertThat(data.semanticActions!!.playOrPause).isNotNull() + assertThat(data.lastActive).isAtLeast(currentTime) + assertThat(data.isExplicit).isTrue() + verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test fun testResumptionDisabled_dismissesResumeControls() { // WHEN there are resume controls and resumption is switched off val desc = diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt index 039dd4d92eb4..e4e95e580a7c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt @@ -20,6 +20,7 @@ import android.app.PendingIntent import android.content.res.Configuration import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.util.MathUtils.abs import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase @@ -31,14 +32,11 @@ import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.pipeline.MediaDataManager -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.PAGINATION_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager +import com.android.systemui.qs.PageIndicator import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider import com.android.systemui.statusbar.policy.ConfigurationController @@ -56,6 +54,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.floatThat import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever @@ -86,6 +85,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var debugLogger: MediaCarouselControllerLogger @Mock lateinit var mediaViewController: MediaViewController @Mock lateinit var smartspaceMediaData: SmartspaceMediaData + @Mock lateinit var mediaCarousel: MediaScrollView + @Mock lateinit var pageIndicator: PageIndicator @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> @Captor lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener> @@ -647,25 +648,22 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Test fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() { val delta = 0.0001F - val paginationSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - val paginationSquishEnd = - TRANSFORM_BEZIER.getInterpolation( - (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION - ) + mediaCarouselController.mediaCarousel = mediaCarousel + mediaCarouselController.pageIndicator = pageIndicator + whenever(mediaCarousel.measuredHeight).thenReturn(100) + whenever(pageIndicator.translationY).thenReturn(80F) + whenever(pageIndicator.height).thenReturn(10) whenever(mediaHostStatesManager.mediaHostStates) .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState)) whenever(mediaHostState.visible).thenReturn(true) mediaCarouselController.currentEndLocation = LOCATION_QS - whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle) + whenever(mediaHostState.squishFraction).thenReturn(0.938F) mediaCarouselController.updatePageIndicatorAlpha() - assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta) + verify(pageIndicator).alpha = floatThat { abs(it - 0.5F) < delta } - whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd) + whenever(mediaHostState.squishFraction).thenReturn(1.0F) mediaCarouselController.updatePageIndicatorAlpha() - assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta) + verify(pageIndicator).alpha = floatThat { abs(it - 1.0F) < delta } } @Ignore("b/253229241") diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index b65f5cb51aaf..cfb19fc32bec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -54,6 +54,7 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.LiveData import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId +import com.android.internal.widget.CachingIconView import com.android.systemui.ActivityIntentHelper import com.android.systemui.R import com.android.systemui.SysuiTestCase @@ -154,6 +155,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var albumView: ImageView private lateinit var titleText: TextView private lateinit var artistText: TextView + private lateinit var explicitIndicator: CachingIconView private lateinit var seamless: ViewGroup private lateinit var seamlessButton: View @Mock private lateinit var seamlessBackground: RippleDrawable @@ -216,6 +218,7 @@ public class MediaControlPanelTest : SysuiTestCase() { this.set(Flags.UMO_SURFACE_RIPPLE, false) this.set(Flags.UMO_TURBULENCE_NOISE, false) this.set(Flags.MEDIA_FALSING_PENALTY, true) + this.set(Flags.MEDIA_EXPLICIT_INDICATOR, true) } @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -350,6 +353,7 @@ public class MediaControlPanelTest : SysuiTestCase() { appIcon = ImageView(context) titleText = TextView(context) artistText = TextView(context) + explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator } seamless = FrameLayout(context) seamless.foreground = seamlessBackground seamlessButton = View(context) @@ -396,6 +400,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(albumView.foreground).thenReturn(mock(Drawable::class.java)) whenever(viewHolder.titleText).thenReturn(titleText) whenever(viewHolder.artistText).thenReturn(artistText) + whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator) whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java)) whenever(viewHolder.seamless).thenReturn(seamless) whenever(viewHolder.seamlessButton).thenReturn(seamlessButton) @@ -1019,6 +1024,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindText() { + useRealConstraintSets() player.attachPlayer(viewHolder) player.bindPlayer(mediaData, PACKAGE) @@ -1036,6 +1042,8 @@ public class MediaControlPanelTest : SysuiTestCase() { handler.onAnimationEnd(mockAnimator) assertThat(titleText.getText()).isEqualTo(TITLE) assertThat(artistText.getText()).isEqualTo(ARTIST) + assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE) + assertThat(collapsedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE) // Rebinding should not trigger animation player.bindPlayer(mediaData, PACKAGE) @@ -1043,6 +1051,36 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + fun bindTextWithExplicitIndicator() { + useRealConstraintSets() + val mediaDataWitExp = mediaData.copy(isExplicit = true) + player.attachPlayer(viewHolder) + player.bindPlayer(mediaDataWitExp, PACKAGE) + + // Capture animation handler + val captor = argumentCaptor<Animator.AnimatorListener>() + verify(mockAnimator, times(2)).addListener(captor.capture()) + val handler = captor.value + + // Validate text views unchanged but animation started + assertThat(titleText.getText()).isEqualTo("") + assertThat(artistText.getText()).isEqualTo("") + verify(mockAnimator, times(1)).start() + + // Binding only after animator runs + handler.onAnimationEnd(mockAnimator) + assertThat(titleText.getText()).isEqualTo(TITLE) + assertThat(artistText.getText()).isEqualTo(ARTIST) + assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.VISIBLE) + assertThat(collapsedSet.getVisibility(explicitIndicator.id)) + .isEqualTo(ConstraintSet.VISIBLE) + + // Rebinding should not trigger animation + player.bindPlayer(mediaData, PACKAGE) + verify(mockAnimator, times(3)).start() + } + + @Test fun bindTextInterrupted() { val data0 = mediaData.copy(artist = "ARTIST_0") val data1 = mediaData.copy(artist = "ARTIST_1") diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 920801f95f5b..a5795184b493 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager @@ -76,6 +77,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle @Mock private lateinit var keyguardViewController: KeyguardViewController + @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController @Captor @@ -110,6 +112,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { keyguardStateController, bypassController, mediaCarouselController, + mediaDataManager, keyguardViewController, dreamOverlayStateController, configurationController, @@ -125,6 +128,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP) setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + whenever(mediaDataManager.hasActiveMedia()).thenReturn(true) whenever(mediaCarouselController.mediaCarouselScrollHandler) .thenReturn(mediaCarouselScrollHandler) val observer = wakefullnessObserver.value @@ -357,17 +361,31 @@ class MediaHierarchyManagerTest : SysuiTestCase() { } @Test - fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue() { + fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_active() { goToLockscreen() enterGuidedTransformation() whenever(lockHost.visible).thenReturn(false) whenever(qsHost.visible).thenReturn(true) whenever(qqsHost.visible).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse() } @Test + fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue_without_active() { + // To keep the appearing behavior, we need to be in a guided transition + goToLockscreen() + enterGuidedTransformation() + whenever(lockHost.visible).thenReturn(false) + whenever(qsHost.visible).thenReturn(true) + whenever(qqsHost.visible).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false) + + assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue() + } + + @Test fun testDream() { goToDream() setMediaDreamComplicationEnabled(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index 35b0eb678441..4ed6d7cf6bd0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -22,13 +22,6 @@ import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY -import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER import com.android.systemui.util.animation.MeasurementInput import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.animation.TransitionViewState @@ -60,9 +53,10 @@ class MediaViewControllerTest : SysuiTestCase() { @Mock private lateinit var controlWidgetState: WidgetState @Mock private lateinit var bgWidgetState: WidgetState @Mock private lateinit var mediaTitleWidgetState: WidgetState + @Mock private lateinit var mediaSubTitleWidgetState: WidgetState @Mock private lateinit var mediaContainerWidgetState: WidgetState - val delta = 0.0001F + val delta = 0.1F private lateinit var mediaViewController: MediaViewController @@ -76,10 +70,11 @@ class MediaViewControllerTest : SysuiTestCase() { @Test fun testObtainViewState_applySquishFraction_toPlayerTransitionViewState_height() { mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) - player.measureState = TransitionViewState().apply { - this.height = 100 - this.measureHeight = 100 - } + player.measureState = + TransitionViewState().apply { + this.height = 100 + this.measureHeight = 100 + } mediaHostStateHolder.expansion = 1f val widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) val heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) @@ -128,29 +123,21 @@ class MediaViewControllerTest : SysuiTestCase() { R.id.header_artist to detailWidgetState ) ) - - val detailSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (DETAILS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, detailSquishMiddle) + whenever(mockCopiedState.measureHeight).thenReturn(200) + // detail widgets occupy [90, 100] + whenever(detailWidgetState.y).thenReturn(90F) + whenever(detailWidgetState.height).thenReturn(10) + // control widgets occupy [150, 170] + whenever(controlWidgetState.y).thenReturn(150F) + whenever(controlWidgetState.height).thenReturn(20) + // in current beizer, when the progress reach 0.38, the result will be 0.5 + mediaViewController.squishViewState(mockViewState, 119F / 200F) verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val detailSquishEnd = - TRANSFORM_BEZIER.getInterpolation((DETAILS_DELAY + DURATION) / ANIMATION_BASE_DURATION) - mediaViewController.squishViewState(mockViewState, detailSquishEnd) + mediaViewController.squishViewState(mockViewState, 150F / 200F) verify(detailWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } - - val controlSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (CONTROLS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, controlSquishMiddle) + mediaViewController.squishViewState(mockViewState, 181.4F / 200F) verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val controlSquishEnd = - TRANSFORM_BEZIER.getInterpolation((CONTROLS_DELAY + DURATION) / ANIMATION_BASE_DURATION) - mediaViewController.squishViewState(mockViewState, controlSquishEnd) + mediaViewController.squishViewState(mockViewState, 200F / 200F) verify(controlWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } } @@ -161,36 +148,33 @@ class MediaViewControllerTest : SysuiTestCase() { .thenReturn( mutableMapOf( R.id.media_title1 to mediaTitleWidgetState, + R.id.media_subtitle1 to mediaSubTitleWidgetState, R.id.media_cover1_container to mediaContainerWidgetState ) ) - - val containerSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (MEDIACONTAINERS_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, containerSquishMiddle) + whenever(mockCopiedState.measureHeight).thenReturn(360) + // media container widgets occupy [20, 300] + whenever(mediaContainerWidgetState.y).thenReturn(20F) + whenever(mediaContainerWidgetState.height).thenReturn(280) + // media title widgets occupy [320, 330] + whenever(mediaTitleWidgetState.y).thenReturn(320F) + whenever(mediaTitleWidgetState.height).thenReturn(10) + // media subtitle widgets occupy [340, 350] + whenever(mediaSubTitleWidgetState.y).thenReturn(340F) + whenever(mediaSubTitleWidgetState.height).thenReturn(10) + + // in current beizer, when the progress reach 0.38, the result will be 0.5 + mediaViewController.squishViewState(mockViewState, 307.6F / 360F) verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val containerSquishEnd = - TRANSFORM_BEZIER.getInterpolation( - (MEDIACONTAINERS_DELAY + DURATION) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, containerSquishEnd) + mediaViewController.squishViewState(mockViewState, 320F / 360F) verify(mediaContainerWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } - - val titleSquishMiddle = - TRANSFORM_BEZIER.getInterpolation( - (MEDIATITLES_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, titleSquishMiddle) + // media title and media subtitle are in same widget group, should be calculate together and + // have same alpha + mediaViewController.squishViewState(mockViewState, 353.8F / 360F) verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } - - val titleSquishEnd = - TRANSFORM_BEZIER.getInterpolation( - (MEDIATITLES_DELAY + DURATION) / ANIMATION_BASE_DURATION - ) - mediaViewController.squishViewState(mockViewState, titleSquishEnd) + verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 0.5F) < delta } + mediaViewController.squishViewState(mockViewState, 360F / 360F) verify(mediaTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } + verify(mediaSubTitleWidgetState).alpha = floatThat { kotlin.math.abs(it - 1.0F) < delta } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index 4cc12c709fa7..f5b3959b322d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -206,6 +206,21 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { } @Test + fun commandQueueCallback_almostCloseToStartCast_deviceNameBlank_showsDefaultDeviceName() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, + routeInfoWithBlankDeviceName, + null, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .contains(context.getString(R.string.media_ttt_default_device_type)) + assertThat(chipbarView.getChipText()) + .isNotEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) + } + + @Test fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, @@ -248,6 +263,21 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { } @Test + fun commandQueueCallback_transferToReceiverTriggered_deviceNameBlank_showsDefaultDeviceName() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, + routeInfoWithBlankDeviceName, + null, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .contains(context.getString(R.string.media_ttt_default_device_type)) + assertThat(chipbarView.getChipText()) + .isNotEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + } + + @Test fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, @@ -934,6 +964,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { private const val APP_NAME = "Fake app name" private const val OTHER_DEVICE_NAME = "My Tablet" +private const val BLANK_DEVICE_NAME = " " private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 @@ -942,3 +973,9 @@ private val routeInfo = .addFeature("feature") .setClientPackageName(PACKAGE_NAME) .build() + +private val routeInfoWithBlankDeviceName = + MediaRoute2Info.Builder("id", BLANK_DEVICE_NAME) + .addFeature("feature") + .setClientPackageName(PACKAGE_NAME) + .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java index 9bf27a26a682..8b0342eda633 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java @@ -51,6 +51,7 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.model.SysUiState; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.shared.recents.utilities.Utilities; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.AutoHideController; import com.android.systemui.statusbar.phone.LightBarController; @@ -102,10 +103,10 @@ public class NavigationBarControllerTest extends SysuiTestCase { mock(NavBarHelper.class), mTaskbarDelegate, mNavigationBarFactory, - mock(StatusBarKeyguardViewManager.class), mock(DumpManager.class), mock(AutoHideController.class), mock(LightBarController.class), + TaskStackChangeListeners.getTestInstance(), Optional.of(mock(Pip.class)), Optional.of(mock(BackAnimation.class)), mock(FeatureFlags.class))); diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java index 80adbf025e0b..2ad865e6ef11 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java @@ -28,6 +28,7 @@ import static android.view.WindowInsets.Type.ime; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS; import static com.android.systemui.navigationbar.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -44,6 +45,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; import android.hardware.display.DisplayManagerGlobal; @@ -90,6 +92,7 @@ import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeController; import com.android.systemui.shared.rotation.RotationButtonController; +import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -203,6 +206,8 @@ public class NavigationBarTest extends SysuiTestCase { private ViewRootImpl mViewRootImpl; private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake(); + private TaskStackChangeListeners mTaskStackChangeListeners = + TaskStackChangeListeners.getTestInstance(); @Rule public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck(); @@ -437,6 +442,14 @@ public class NavigationBarTest extends SysuiTestCase { verify(mNavBarHelper, times(1)).getCurrentSysuiState(); } + @Test + public void testScreenPinningEnabled_updatesSysuiState() { + mNavigationBar.init(); + mTaskStackChangeListeners.getListenerImpl().onLockTaskModeChanged( + ActivityManager.LOCK_TASK_MODE_PINNED); + verify(mMockSysUiState).setFlag(eq(SYSUI_STATE_SCREEN_PINNING), eq(true)); + } + private NavigationBar createNavBar(Context context) { DeviceProvisionedController deviceProvisionedController = mock(DeviceProvisionedController.class); @@ -481,7 +494,8 @@ public class NavigationBarTest extends SysuiTestCase { mEdgeBackGestureHandler, Optional.of(mock(BackAnimation.class)), mUserContextProvider, - mWakefulnessLifecycle)); + mWakefulnessLifecycle, + mTaskStackChangeListeners)); } private void processAllMessages() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt index 1742c6994246..537dfb821fef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt @@ -1,11 +1,14 @@ package com.android.systemui.navigationbar +import android.app.ActivityManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.model.SysUiState import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler import com.android.systemui.recents.OverviewProxyService +import com.android.systemui.shared.system.QuickStepContract +import com.android.systemui.shared.system.TaskStackChangeListeners import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.phone.AutoHideController import com.android.systemui.statusbar.phone.LightBarController @@ -14,6 +17,7 @@ import com.android.wm.shell.back.BackAnimation import com.android.wm.shell.pip.Pip import org.junit.Before import org.junit.Test +import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.any @@ -30,6 +34,7 @@ class TaskbarDelegateTest : SysuiTestCase() { val MODE_GESTURE = 0; val MODE_THREE_BUTTON = 1; + private lateinit var mTaskStackChangeListeners: TaskStackChangeListeners private lateinit var mTaskbarDelegate: TaskbarDelegate @Mock lateinit var mEdgeBackGestureHandlerFactory : EdgeBackGestureHandler.Factory @@ -69,11 +74,12 @@ class TaskbarDelegateTest : SysuiTestCase() { `when`(mLightBarControllerFactory.create(any())).thenReturn(mLightBarTransitionController) `when`(mNavBarHelper.currentSysuiState).thenReturn(mCurrentSysUiState) `when`(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState) + mTaskStackChangeListeners = TaskStackChangeListeners.getTestInstance() mTaskbarDelegate = TaskbarDelegate(context, mEdgeBackGestureHandlerFactory, mLightBarControllerFactory) mTaskbarDelegate.setDependencies(mCommandQueue, mOverviewProxyService, mNavBarHelper, mNavigationModeController, mSysUiState, mDumpManager, mAutoHideController, - mLightBarController, mOptionalPip, mBackAnimation) + mLightBarController, mOptionalPip, mBackAnimation, mTaskStackChangeListeners) } @Test @@ -90,4 +96,15 @@ class TaskbarDelegateTest : SysuiTestCase() { mTaskbarDelegate.init(DISPLAY_ID) verify(mEdgeBackGestureHandler, times(1)).onNavigationModeChanged(MODE_GESTURE) } + + @Test + fun screenPinningEnabled_updatesSysuiState() { + mTaskbarDelegate.init(DISPLAY_ID) + mTaskStackChangeListeners.listenerImpl.onLockTaskModeChanged( + ActivityManager.LOCK_TASK_MODE_PINNED) + verify(mSysUiState, times(1)).setFlag( + ArgumentMatchers.eq(QuickStepContract.SYSUI_STATE_SCREEN_PINNING), + ArgumentMatchers.eq(true) + ) + } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 4a9c7508b1b3..8440455127bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -24,7 +24,7 @@ import android.os.UserManager import android.test.suitebuilder.annotation.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase -import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq @@ -50,7 +50,7 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) internal class NoteTaskControllerTest : SysuiTestCase() { - private val notesIntent = Intent(NOTES_ACTION) + private val notesIntent = Intent(ACTION_CREATE_NOTE) @Mock lateinit var context: Context @Mock lateinit var packageManager: PackageManager @@ -93,7 +93,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -102,7 +102,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) - verify(bubbles).showAppBubble(notesIntent) + verify(bubbles).showOrHideAppBubble(notesIntent) verify(context, never()).startActivity(notesIntent) } @@ -113,7 +113,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = true) verify(context).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -123,7 +123,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -133,7 +133,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -143,7 +143,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -153,7 +153,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -161,7 +161,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController(isEnabled = false).showNoteTask() verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -171,7 +171,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } // endregion diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt index 538131a4dd73..010ac5bbb2d9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -106,7 +106,9 @@ internal class NoteTaskInitializerTest : SysuiTestCase() { // region handleSystemKey @Test fun handleSystemKey_receiveValidSystemKey_shouldShowNoteTask() { - createNoteTaskInitializer().callbacks.handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + createNoteTaskInitializer() + .callbacks + .handleSystemKey(NoteTaskController.NOTE_TASK_KEY_EVENT) verify(noteTaskController).showNoteTask() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt index dd2cc2ffc9db..bbe60f4ba493 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt @@ -23,11 +23,10 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags import android.content.pm.ResolveInfo -import android.content.pm.ServiceInfo import android.test.suitebuilder.annotation.SmallTest import androidx.test.runner.AndroidJUnit4 import com.android.systemui.SysuiTestCase -import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.ACTION_CREATE_NOTE import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -58,19 +57,13 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { } private fun createResolveInfo( - packageName: String = "PackageName", - activityInfo: ActivityInfo? = null, + activityInfo: ActivityInfo? = createActivityInfo(), ): ResolveInfo { - return ResolveInfo().apply { - serviceInfo = - ServiceInfo().apply { - applicationInfo = ApplicationInfo().apply { this.packageName = packageName } - } - this.activityInfo = activityInfo - } + return ResolveInfo().apply { this.activityInfo = activityInfo } } private fun createActivityInfo( + packageName: String = "PackageName", name: String? = "ActivityName", exported: Boolean = true, enabled: Boolean = true, @@ -87,6 +80,7 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { if (turnScreenOn) { flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON } + this.applicationInfo = ApplicationInfo().apply { this.packageName = packageName } } } @@ -107,7 +101,8 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { val actual = resolver.resolveIntent() val expected = - Intent(NOTES_ACTION) + Intent(ACTION_CREATE_NOTE) + .setPackage("PackageName") .setComponent(ComponentName("PackageName", "ActivityName")) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) // Compares the string representation of both intents, as they are different instances. @@ -204,7 +199,9 @@ internal class NoteTaskIntentResolverTest : SysuiTestCase() { @Test fun resolveIntent_packageNameIsBlank_shouldReturnNull() { - givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) } + givenQueryIntentActivities { + listOf(createResolveInfo(createActivityInfo(packageName = ""))) + } val actual = resolver.resolveIntent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt index ca3182affcc1..3281fa9bd8a4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt @@ -28,7 +28,6 @@ import com.android.systemui.qs.tiles.BatterySaverTile import com.android.systemui.qs.tiles.BluetoothTile import com.android.systemui.qs.tiles.CameraToggleTile import com.android.systemui.qs.tiles.CastTile -import com.android.systemui.qs.tiles.CellularTile import com.android.systemui.qs.tiles.ColorCorrectionTile import com.android.systemui.qs.tiles.ColorInversionTile import com.android.systemui.qs.tiles.DataSaverTile @@ -49,7 +48,6 @@ import com.android.systemui.qs.tiles.ReduceBrightColorsTile import com.android.systemui.qs.tiles.RotationLockTile import com.android.systemui.qs.tiles.ScreenRecordTile import com.android.systemui.qs.tiles.UiModeNightTile -import com.android.systemui.qs.tiles.WifiTile import com.android.systemui.qs.tiles.WorkModeTile import com.android.systemui.util.leak.GarbageMonitor import com.google.common.truth.Truth.assertThat @@ -63,10 +61,8 @@ import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever private val specMap = mapOf( - "wifi" to WifiTile::class.java, "internet" to InternetTile::class.java, "bt" to BluetoothTile::class.java, - "cell" to CellularTile::class.java, "dnd" to DndTile::class.java, "inversion" to ColorInversionTile::class.java, "airplane" to AirplaneModeTile::class.java, @@ -102,10 +98,8 @@ class QSFactoryImplTest : SysuiTestCase() { @Mock(answer = Answers.RETURNS_SELF) private lateinit var customTileBuilder: CustomTile.Builder @Mock private lateinit var customTile: CustomTile - @Mock private lateinit var wifiTile: WifiTile @Mock private lateinit var internetTile: InternetTile @Mock private lateinit var bluetoothTile: BluetoothTile - @Mock private lateinit var cellularTile: CellularTile @Mock private lateinit var dndTile: DndTile @Mock private lateinit var colorInversionTile: ColorInversionTile @Mock private lateinit var airplaneTile: AirplaneModeTile @@ -146,10 +140,8 @@ class QSFactoryImplTest : SysuiTestCase() { factory = QSFactoryImpl( { qsHost }, { customTileBuilder }, - { wifiTile }, { internetTile }, { bluetoothTile }, - { cellularTile }, { dndTile }, { colorInversionTile }, { airplaneTile }, diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt new file mode 100644 index 000000000000..3710281499b3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt @@ -0,0 +1,100 @@ +package com.android.systemui.settings + +import android.content.Context +import android.content.Intent +import android.content.pm.UserInfo +import android.os.Handler +import android.os.UserHandle +import android.os.UserManager +import androidx.concurrent.futures.DirectExecutor +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(Parameterized::class) +class UserTrackerImplReceiveTest : SysuiTestCase() { + + companion object { + + @JvmStatic + @Parameterized.Parameters + fun data(): Iterable<String> = + listOf( + Intent.ACTION_USER_INFO_CHANGED, + Intent.ACTION_MANAGED_PROFILE_AVAILABLE, + Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE, + Intent.ACTION_MANAGED_PROFILE_ADDED, + Intent.ACTION_MANAGED_PROFILE_REMOVED, + Intent.ACTION_MANAGED_PROFILE_UNLOCKED + ) + } + + private val executor: Executor = DirectExecutor.INSTANCE + + @Mock private lateinit var context: Context + @Mock private lateinit var userManager: UserManager + @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager + @Mock(stubOnly = true) private lateinit var handler: Handler + + @Parameterized.Parameter lateinit var intentAction: String + @Mock private lateinit var callback: UserTracker.Callback + @Captor private lateinit var captor: ArgumentCaptor<List<UserInfo>> + + private lateinit var tracker: UserTrackerImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + `when`(context.user).thenReturn(UserHandle.SYSTEM) + `when`(context.createContextAsUser(ArgumentMatchers.any(), anyInt())).thenReturn(context) + + tracker = UserTrackerImpl(context, userManager, dumpManager, handler) + } + + @Test + fun `calls callback and updates profiles when an intent received`() { + tracker.initialize(0) + tracker.addCallback(callback, executor) + val profileID = tracker.userId + 10 + + `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> + val id = invocation.getArgument<Int>(0) + val info = UserInfo(id, "", UserInfo.FLAG_FULL) + val infoProfile = + UserInfo( + id + 10, + "", + "", + UserInfo.FLAG_MANAGED_PROFILE, + UserManager.USER_TYPE_PROFILE_MANAGED + ) + infoProfile.profileGroupId = id + listOf(info, infoProfile) + } + + tracker.onReceive(context, Intent(intentAction)) + + verify(callback, times(0)).onUserChanged(anyInt(), any()) + verify(callback, times(1)).onProfilesChanged(capture(captor)) + assertThat(captor.value.map { it.id }).containsExactly(0, profileID) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt index 52462c7186d4..e65bbb1bea08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt @@ -124,6 +124,16 @@ class UserTrackerImplTest : SysuiTestCase() { verify(context).registerReceiverForAllUsers( eq(tracker), capture(captor), isNull(), eq(handler)) + with(captor.value) { + assertThat(countActions()).isEqualTo(7) + assertThat(hasAction(Intent.ACTION_USER_SWITCHED)).isTrue() + assertThat(hasAction(Intent.ACTION_USER_INFO_CHANGED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_ADDED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_REMOVED)).isTrue() + assertThat(hasAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED)).isTrue() + } } @Test @@ -280,37 +290,6 @@ class UserTrackerImplTest : SysuiTestCase() { } @Test - fun testCallbackCalledOnProfileChanged() { - tracker.initialize(0) - val callback = TestCallback() - tracker.addCallback(callback, executor) - val profileID = tracker.userId + 10 - - `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> - val id = invocation.getArgument<Int>(0) - val info = UserInfo(id, "", UserInfo.FLAG_FULL) - val infoProfile = UserInfo( - id + 10, - "", - "", - UserInfo.FLAG_MANAGED_PROFILE, - UserManager.USER_TYPE_PROFILE_MANAGED - ) - infoProfile.profileGroupId = id - listOf(info, infoProfile) - } - - val intent = Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) - .putExtra(Intent.EXTRA_USER, UserHandle.of(profileID)) - - tracker.onReceive(context, intent) - - assertThat(callback.calledOnUserChanged).isEqualTo(0) - assertThat(callback.calledOnProfilesChanged).isEqualTo(1) - assertThat(callback.lastUserProfiles.map { it.id }).containsExactly(0, profileID) - } - - @Test fun testCallbackCalledOnUserInfoChanged() { tracker.initialize(0) val callback = TestCallback() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt index 88651c1292c3..f802a5e09228 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/CombinedShadeHeaderConstraintsTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade import android.testing.AndroidTestingRunner +import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START @@ -92,12 +93,12 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { assertThat(getConstraint(R.id.clock).layout.horizontalBias).isEqualTo(0f) assertThat(getConstraint(R.id.date).layout.startToStart).isEqualTo(PARENT_ID) - assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0f) + assertThat(getConstraint(R.id.date).layout.horizontalBias).isEqualTo(0.5f) assertThat(getConstraint(R.id.batteryRemainingIcon).layout.endToEnd) .isEqualTo(PARENT_ID) assertThat(getConstraint(R.id.batteryRemainingIcon).layout.horizontalBias) - .isEqualTo(1f) + .isEqualTo(0.5f) assertThat(getConstraint(R.id.privacy_container).layout.endToEnd) .isEqualTo(R.id.end_guide) @@ -331,10 +332,8 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { val views = mapOf( R.id.clock to "clock", R.id.date to "date", - R.id.statusIcons to "icons", R.id.privacy_container to "privacy", R.id.carrier_group to "carriers", - R.id.batteryRemainingIcon to "battery", ) views.forEach { (id, name) -> assertWithMessage("$name has 0 height in qqs") @@ -352,11 +351,8 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { fun testCheckViewsDontChangeSizeBetweenAnimationConstraints() { val views = mapOf( R.id.clock to "clock", - R.id.date to "date", - R.id.statusIcons to "icons", R.id.privacy_container to "privacy", R.id.carrier_group to "carriers", - R.id.batteryRemainingIcon to "battery", ) views.forEach { (id, name) -> expect.withMessage("$name changes height") @@ -369,8 +365,8 @@ class CombinedShadeHeaderConstraintsTest : SysuiTestCase() { } private fun Int.fromConstraint() = when (this) { - -1 -> "MATCH_PARENT" - -2 -> "WRAP_CONTENT" + ViewGroup.LayoutParams.MATCH_PARENT -> "MATCH_PARENT" + ViewGroup.LayoutParams.WRAP_CONTENT -> "WRAP_CONTENT" else -> toString() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 896db4b43eb2..0f3d4a8ca59a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -103,9 +103,11 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; +import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel; @@ -297,9 +299,11 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel; @Mock private LockscreenToDreamingTransitionViewModel mLockscreenToDreamingTransitionViewModel; @Mock private LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel; + @Mock private GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel; @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; @Mock private CoroutineDispatcher mMainDispatcher; + @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private MotionEvent mDownMotionEvent; @Captor private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> @@ -516,9 +520,11 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { systemClock, mKeyguardBottomAreaViewModel, mKeyguardBottomAreaInteractor, + mAlternateBouncerInteractor, mDreamingToLockscreenTransitionViewModel, mOccludedToLockscreenTransitionViewModel, mLockscreenToDreamingTransitionViewModel, + mGoneToDreamingTransitionViewModel, mLockscreenToOccludedTransitionViewModel, mMainDispatcher, mKeyguardTransitionInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 08a9c9664ae0..526dc8d150fe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -46,11 +46,14 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; +import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -68,6 +71,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import java.util.List; + @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest @@ -91,13 +96,21 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Mock private ShadeExpansionStateManager mShadeExpansionStateManager; @Mock private ShadeWindowLogger mShadeWindowLogger; @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters; + @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener; private NotificationShadeWindowControllerImpl mNotificationShadeWindowController; - + private float mPreferredRefreshRate = -1; @Before public void setUp() { MockitoAnnotations.initMocks(this); + // Preferred refresh rate is equal to the first displayMode's refresh rate + mPreferredRefreshRate = mContext.getDisplay().getSupportedModes()[0].getRefreshRate(); + overrideResource( + R.integer.config_keyguardRefreshRate, + (int) mPreferredRefreshRate + ); + when(mDozeParameters.getAlwaysOn()).thenReturn(true); when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors); @@ -117,6 +130,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mNotificationShadeWindowController.attach(); verify(mWindowManager).addView(eq(mNotificationShadeWindowView), any()); + verify(mStatusBarStateController).addCallback(mStateListener.capture(), anyInt()); } @Test @@ -334,4 +348,59 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { assertThat(mLayoutParameters.getValue().screenOrientation) .isEqualTo(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); } + + @Test + public void udfpsEnrolled_minAndMaxRefreshRateSetToPreferredRefreshRate() { + // GIVEN udfps is enrolled + when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true); + + // WHEN keyguard is showing + setKeyguardShowing(); + + // THEN min and max refresh rate is set to the preferredRefreshRate + verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture()); + final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues(); + final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1); + assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(mPreferredRefreshRate); + assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(mPreferredRefreshRate); + } + + @Test + public void udfpsNotEnrolled_refreshRateUnset() { + // GIVEN udfps is NOT enrolled + when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false); + + // WHEN keyguard is showing + setKeyguardShowing(); + + // THEN min and max refresh rate aren't set (set to 0) + verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture()); + final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues(); + final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1); + assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0); + assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0); + } + + @Test + public void keyguardNotShowing_refreshRateUnset() { + // GIVEN UDFPS is enrolled + when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true); + + // WHEN keyguard is NOT showing + mNotificationShadeWindowController.setKeyguardShowing(false); + + // THEN min and max refresh rate aren't set (set to 0) + verify(mWindowManager, atLeastOnce()).updateViewLayout(any(), mLayoutParameters.capture()); + final List<WindowManager.LayoutParams> lpList = mLayoutParameters.getAllValues(); + final WindowManager.LayoutParams lp = lpList.get(lpList.size() - 1); + assertThat(lp.preferredMaxDisplayRefreshRate).isEqualTo(0); + assertThat(lp.preferredMinDisplayRefreshRate).isEqualTo(0); + } + + private void setKeyguardShowing() { + mNotificationShadeWindowController.setKeyguardShowing(true); + mNotificationShadeWindowController.setKeyguardGoingAway(false); + mNotificationShadeWindowController.setKeyguardFadingAway(false); + mStateListener.getValue().onStateChanged(StatusBarState.KEYGUARD); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index d5e6463dd380..4c768253202a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.dock.DockManager import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler @@ -98,6 +99,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { private lateinit var pulsingGestureListener: PulsingGestureListener @Mock private lateinit var notificationInsetsController: NotificationInsetsController + @Mock + private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory @Mock lateinit var keyguardBouncerContainer: ViewGroup @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent @@ -134,8 +137,9 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { pulsingGestureListener, featureFlags, keyguardBouncerViewModel, - keyguardTransitionInteractor, keyguardBouncerComponentFactory, + alternateBouncerInteractor, + keyguardTransitionInteractor, ) underTest.setupExpandedStatusBar() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java index f1d8188612a6..d43562443d6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java @@ -40,6 +40,7 @@ import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.dock.DockManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel; import com.android.systemui.statusbar.DragDownHelper; @@ -94,6 +95,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { @Mock private KeyguardBouncerViewModel mKeyguardBouncerViewModel; @Mock private KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory; @Mock private NotificationInsetsController mNotificationInsetsController; + @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; @Captor private ArgumentCaptor<NotificationShadeWindowView.InteractionEventHandler> @@ -134,8 +136,9 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { mPulsingGestureListener, mFeatureFlags, mKeyguardBouncerViewModel, - mKeyguardTransitionInteractor, - mKeyguardBouncerComponentFactory + mKeyguardBouncerComponentFactory, + mAlternateBouncerInteractor, + mKeyguardTransitionInteractor ); mController.setupExpandedStatusBar(); mController.setDragDownHelper(mDragDownHelper); @@ -158,7 +161,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false); // THEN we should intercept touch @@ -171,7 +174,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { // WHEN not showing alt auth, not dozing, drag down helper doesn't want to intercept when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(false); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(false); when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false); // THEN we shouldn't intercept touch @@ -184,7 +187,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarKeyguardViewManager.isShowingAlternateBouncer()).thenReturn(true); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false); // THEN we should handle the touch 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 d2dd43308fcc..610bb13c6016 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java @@ -99,6 +99,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardIndication; import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController; import com.android.systemui.keyguard.ScreenLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.phone.KeyguardBypassController; @@ -177,6 +178,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { @Mock private FaceHelpMessageDeferral mFaceHelpMessageDeferral; @Mock + private AlternateBouncerInteractor mAlternateBouncerInteractor; + @Mock private ScreenLifecycle mScreenLifecycle; @Mock private AuthController mAuthController; @@ -273,7 +276,8 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase { mUserManager, mExecutor, mExecutor, mFalsingManager, mAuthController, mLockPatternUtils, mScreenLifecycle, mKeyguardBypassController, mAccessibilityManager, - mFaceHelpMessageDeferral, mock(KeyguardLogger.class)); + mFaceHelpMessageDeferral, mock(KeyguardLogger.class), + mAlternateBouncerInteractor); mController.init(); mController.setIndicationArea(mIndicationArea); verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index ca99e24fc105..e41929f7d578 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; import org.junit.Assert; import org.junit.Before; @@ -216,4 +217,29 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { Assert.assertEquals(1f, mChildrenContainer.getBottomRoundness(), 0.001f); Assert.assertEquals(1f, notificationRow.getBottomRoundness(), 0.001f); } + + @Test + public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_header() { + mChildrenContainer.useRoundnessSourceTypes(true); + + NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper(); + Assert.assertEquals(0f, header.getTopRoundness(), 0.001f); + + mChildrenContainer.requestTopRoundness(1f, SourceType.from(""), false); + + Assert.assertEquals(1f, header.getTopRoundness(), 0.001f); + } + + @Test + public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_headerLowPriority() { + mChildrenContainer.useRoundnessSourceTypes(true); + mChildrenContainer.setIsLowPriority(true); + + NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper(); + Assert.assertEquals(0f, header.getTopRoundness(), 0.001f); + + mChildrenContainer.requestTopRoundness(1f, SourceType.from(""), false); + + Assert.assertEquals(1f, header.getTopRoundness(), 0.001f); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java index 4ccbc6d45e63..091bb5455d93 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doReturn; @@ -74,6 +75,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; +import org.mockito.stubbing.Answer; import java.util.Collections; import java.util.List; @@ -115,8 +117,10 @@ public class AutoTileManagerTest extends SysuiTestCase { @Spy private PackageManager mPackageManager; private final boolean mIsReduceBrightColorsAvailable = true; - private AutoTileManager mAutoTileManager; + private AutoTileManager mAutoTileManager; // under test + private SecureSettings mSecureSettings; + private ManagedProfileController.Callback mManagedProfileCallback; @Before public void setUp() throws Exception { @@ -303,7 +307,7 @@ public class AutoTileManagerTest extends SysuiTestCase { InOrder inOrderManagedProfile = inOrder(mManagedProfileController); inOrderManagedProfile.verify(mManagedProfileController).removeCallback(any()); - inOrderManagedProfile.verify(mManagedProfileController, never()).addCallback(any()); + inOrderManagedProfile.verify(mManagedProfileController).addCallback(any()); if (ColorDisplayManager.isNightDisplayAvailable(mContext)) { InOrder inOrderNightDisplay = inOrder(mNightDisplayListener); @@ -504,6 +508,40 @@ public class AutoTileManagerTest extends SysuiTestCase { } @Test + public void managedProfileAdded_tileAdded() { + when(mAutoAddTracker.isAdded(eq("work"))).thenReturn(false); + mAutoTileManager = createAutoTileManager(mContext); + Mockito.doAnswer((Answer<Object>) invocation -> { + mManagedProfileCallback = invocation.getArgument(0); + return null; + }).when(mManagedProfileController).addCallback(any()); + mAutoTileManager.init(); + when(mManagedProfileController.hasActiveProfile()).thenReturn(true); + + mManagedProfileCallback.onManagedProfileChanged(); + + verify(mQsTileHost, times(1)).addTile(eq("work")); + verify(mAutoAddTracker, times(1)).setTileAdded(eq("work")); + } + + @Test + public void managedProfileRemoved_tileRemoved() { + when(mAutoAddTracker.isAdded(eq("work"))).thenReturn(true); + mAutoTileManager = createAutoTileManager(mContext); + Mockito.doAnswer((Answer<Object>) invocation -> { + mManagedProfileCallback = invocation.getArgument(0); + return null; + }).when(mManagedProfileController).addCallback(any()); + mAutoTileManager.init(); + when(mManagedProfileController.hasActiveProfile()).thenReturn(false); + + mManagedProfileCallback.onManagedProfileChanged(); + + verify(mQsTileHost, times(1)).removeTile(eq("work")); + verify(mAutoAddTracker, times(1)).setTileRemoved(eq("work")); + } + + @Test public void testEmptyArray_doesNotCrash() { mContext.getOrCreateTestableResources().addOverride( R.array.config_quickSettingsAutoAdd, new String[0]); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 74f8c61ad186..daf7dd06b0d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -57,6 +59,7 @@ import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -122,6 +125,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { private VibratorHelper mVibratorHelper; @Mock private BiometricUnlockLogger mLogger; + private final FakeSystemClock mSystemClock = new FakeSystemClock(); private BiometricUnlockController mBiometricUnlockController; @Before @@ -144,7 +148,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mMetricsLogger, mDumpManager, mPowerManager, mLogger, mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle, mAuthController, mStatusBarStateController, mKeyguardUnlockAnimationController, - mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper); + mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper, + mSystemClock + ); mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); mBiometricUnlockController.addBiometricModeListener(mBiometricModeListener); when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker); @@ -207,7 +213,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { verify(mKeyguardViewMediator).onWakeAndUnlocking(); assertThat(mBiometricUnlockController.getMode()) - .isEqualTo(BiometricUnlockController.MODE_WAKE_AND_UNLOCK); + .isEqualTo(MODE_WAKE_AND_UNLOCK); } @Test @@ -457,4 +463,83 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { // THEN wakeup the device verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString()); } + + @Test + public void onSideFingerprintSuccess_recentPowerButtonPress_noHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + + // GIVEN last wake time just occurred + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN DO NOT vibrate the device + verify(mVibratorHelper, never()).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + + // GIVEN last wake time was 500ms ago + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + mSystemClock.advanceTime(500); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN vibrate the device + verify(mVibratorHelper).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() { + // GIVEN side fingerprint enrolled, wakeup just happened + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // GIVEN last wake reason was from a gesture + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_GESTURE); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN vibrate the device + verify(mVibratorHelper).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintFail_alwaysPlaysHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was recent power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // WHEN biometric fingerprint fails + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + + // THEN always vibrate the device + verify(mVibratorHelper).vibrateAuthError(anyString()); + } + + private void givenFingerprintModeUnlockCollapsing() { + when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 09254ad0faf2..c8157ccc8a9a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -110,6 +110,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; @@ -177,8 +178,6 @@ import com.android.systemui.volume.VolumeComponent; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.startingsurface.StartingSurface; -import dagger.Lazy; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -191,6 +190,8 @@ import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.util.Optional; +import dagger.Lazy; + @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -297,6 +298,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private WiredChargingRippleController mWiredChargingRippleController; @Mock private Lazy<CameraLauncher> mCameraLauncherLazy; @Mock private CameraLauncher mCameraLauncher; + @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; /** * The process of registering/unregistering a predictive back callback requires a * ViewRootImpl, which is present IRL, but may be missing during a Mockito unit test. @@ -378,7 +380,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any()); mWakefulnessLifecycle = - new WakefulnessLifecycle(mContext, mIWallpaperManager, mDumpManager); + new WakefulnessLifecycle(mContext, mIWallpaperManager, mFakeSystemClock, + mDumpManager); mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN); mWakefulnessLifecycle.dispatchFinishedWakingUp(); @@ -504,7 +507,9 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mWiredChargingRippleController, mDreamManager, mCameraLauncherLazy, - () -> mLightRevealScrimViewModel) { + () -> mLightRevealScrimViewModel, + mAlternateBouncerInteractor + ) { @Override protected ViewRootImpl getViewRootImpl() { return mViewRootImpl; 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..c8438501b3e6 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 @@ -23,6 +23,10 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.res.Resources; @@ -39,10 +43,9 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; import com.android.systemui.unfold.FoldAodAnimationController; @@ -52,6 +55,8 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -69,7 +74,6 @@ public class DozeParametersTest extends SysuiTestCase { @Mock private PowerManager mPowerManager; @Mock private TunerService mTunerService; @Mock private BatteryController mBatteryController; - @Mock private FeatureFlags mFeatureFlags; @Mock private DumpManager mDumpManager; @Mock private ScreenOffAnimationController mScreenOffAnimationController; @Mock private FoldAodAnimationController mFoldAodAnimationController; @@ -78,6 +82,7 @@ public class DozeParametersTest extends SysuiTestCase { @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private StatusBarStateController mStatusBarStateController; @Mock private ConfigurationController mConfigurationController; + @Captor private ArgumentCaptor<BatteryStateChangeCallback> mBatteryStateChangeCallback; /** * The current value of PowerManager's dozeAfterScreenOff property. @@ -113,7 +118,6 @@ public class DozeParametersTest extends SysuiTestCase { mBatteryController, mTunerService, mDumpManager, - mFeatureFlags, mScreenOffAnimationController, Optional.of(mSysUIUnfoldComponent), mUnlockedScreenOffAnimationController, @@ -122,7 +126,8 @@ public class DozeParametersTest extends SysuiTestCase { mStatusBarStateController ); - when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(true); + verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture()); + setAodEnabledForTest(true); setShouldControlUnlockedScreenOffForTest(true); setDisplayNeedsBlankingForTest(false); @@ -173,6 +178,29 @@ public class DozeParametersTest extends SysuiTestCase { assertThat(mDozeParameters.getAlwaysOn()).isFalse(); } + @Test + public void testGetAlwaysOn_whenBatterySaverCallback() { + DozeParameters.Callback callback = mock(DozeParameters.Callback.class); + mDozeParameters.addCallback(callback); + + when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mBatteryController.isAodPowerSave()).thenReturn(true); + + // Both lines should trigger an event + mDozeParameters.onTuningChanged(Settings.Secure.DOZE_ALWAYS_ON, "1"); + mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); + + verify(callback, times(2)).onAlwaysOnChange(); + assertThat(mDozeParameters.getAlwaysOn()).isFalse(); + + reset(callback); + when(mBatteryController.isAodPowerSave()).thenReturn(false); + mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); + + verify(callback).onAlwaysOnChange(); + assertThat(mDozeParameters.getAlwaysOn()).isTrue(); + } + /** * PowerManager.setDozeAfterScreenOff(true) means we are not controlling screen off, and calling * it with false means we are. Confusing, but sure - make sure that we call PowerManager with @@ -196,17 +224,6 @@ public class DozeParametersTest extends SysuiTestCase { } @Test - public void testControlUnlockedScreenOffAnimationDisabled_dozeAfterScreenOff() { - when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(false); - - assertFalse(mDozeParameters.shouldControlUnlockedScreenOff()); - - // Trigger the setter for the current value. - mDozeParameters.setControlScreenOffAnimation(mDozeParameters.shouldControlScreenOff()); - assertFalse(mDozeParameters.shouldControlScreenOff()); - } - - @Test public void propagatesAnimateScreenOff_noAlwaysOn() { setAodEnabledForTest(false); setDisplayNeedsBlankingForTest(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 14a319bc87e9..04a67006d686 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -56,6 +56,7 @@ import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.data.BouncerView; import com.android.systemui.keyguard.data.BouncerViewDelegate; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.navigationbar.NavigationModeController; @@ -105,7 +106,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private KeyguardBouncer.Factory mKeyguardBouncerFactory; @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController; - @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer; @Mock private KeyguardMessageArea mKeyguardMessageArea; @Mock private ShadeController mShadeController; @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent; @@ -115,6 +115,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private KeyguardSecurityModel mKeyguardSecurityModel; @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; + @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private BouncerView mBouncerView; @Mock private BouncerViewDelegate mBouncerViewDelegate; @@ -163,7 +164,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mFeatureFlags, mPrimaryBouncerCallbackInteractor, mPrimaryBouncerInteractor, - mBouncerView) { + mBouncerView, + mAlternateBouncerInteractor) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; @@ -434,37 +436,35 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test public void testShowing_whenAlternateAuthShowing() { - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); assertTrue( - "Is showing not accurate when alternative auth showing", + "Is showing not accurate when alternative bouncer is visible", mStatusBarKeyguardViewManager.isBouncerShowing()); } @Test public void testWillBeShowing_whenAlternateAuthShowing() { - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); assertTrue( - "Is or will be showing not accurate when alternative auth showing", + "Is or will be showing not accurate when alternate bouncer is visible", mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()); } @Test - public void testHideAlternateBouncer_onShowBouncer() { - // GIVEN alt auth is showing - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + public void testHideAlternateBouncer_onShowPrimaryBouncer() { + reset(mAlternateBouncerInteractor); + + // GIVEN alt bouncer is showing when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); - reset(mAlternateBouncer); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); // WHEN showBouncer is called mStatusBarKeyguardViewManager.showPrimaryBouncer(true); // THEN alt bouncer should be hidden - verify(mAlternateBouncer).hideAlternateBouncer(); + verify(mAlternateBouncerInteractor).hide(); } @Test @@ -479,11 +479,9 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test public void testShowAltAuth_unlockingWithBiometricNotAllowed() { - // GIVEN alt auth exists, unlocking with biometric isn't allowed - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + // GIVEN cannot use alternate bouncer when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); - when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) - .thenReturn(false); + when(mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()).thenReturn(false); // WHEN showGenericBouncer is called final boolean scrimmed = true; @@ -491,21 +489,19 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { // THEN regular bouncer is shown verify(mPrimaryBouncerInteractor).show(eq(scrimmed)); - verify(mAlternateBouncer, never()).showAlternateBouncer(); } @Test public void testShowAlternateBouncer_unlockingWithBiometricAllowed() { - // GIVEN alt auth exists, unlocking with biometric is allowed - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); + // GIVEN will show alternate bouncer when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(false); - when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + when(mAlternateBouncerInteractor.show()).thenReturn(true); // WHEN showGenericBouncer is called mStatusBarKeyguardViewManager.showBouncer(true); // THEN alt auth bouncer is shown - verify(mAlternateBouncer).showAlternateBouncer(); + verify(mAlternateBouncerInteractor).show(); verify(mPrimaryBouncerInteractor, never()).show(anyBoolean()); } @@ -613,7 +609,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mFeatureFlags, mPrimaryBouncerCallbackInteractor, mPrimaryBouncerInteractor, - mBouncerView) { + mBouncerView, + mAlternateBouncerInteractor) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java index 96fba39d6b59..a9c55fa06cc5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest_Old.java @@ -56,6 +56,7 @@ import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.data.BouncerView; import com.android.systemui.keyguard.data.BouncerViewDelegate; +import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerCallbackInteractor; import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.navigationbar.NavigationModeController; @@ -109,7 +110,6 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController; @Mock private KeyguardBouncer mPrimaryBouncer; - @Mock private StatusBarKeyguardViewManager.AlternateBouncer mAlternateBouncer; @Mock private KeyguardMessageArea mKeyguardMessageArea; @Mock private ShadeController mShadeController; @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent; @@ -119,6 +119,7 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { @Mock private KeyguardSecurityModel mKeyguardSecurityModel; @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; + @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private BouncerView mBouncerView; @Mock private BouncerViewDelegate mBouncerViewDelegate; @@ -169,7 +170,8 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { mFeatureFlags, mPrimaryBouncerCallbackInteractor, mPrimaryBouncerInteractor, - mBouncerView) { + mBouncerView, + mAlternateBouncerInteractor) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; @@ -439,41 +441,6 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { } @Test - public void testShowing_whenAlternateAuthShowing() { - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); - assertTrue( - "Is showing not accurate when alternative auth showing", - mStatusBarKeyguardViewManager.isBouncerShowing()); - } - - @Test - public void testWillBeShowing_whenAlternateAuthShowing() { - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); - assertTrue( - "Is or will be showing not accurate when alternative auth showing", - mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()); - } - - @Test - public void testHideAlternateBouncer_onShowBouncer() { - // GIVEN alt auth is showing - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mAlternateBouncer.isShowingAlternateBouncer()).thenReturn(true); - reset(mAlternateBouncer); - - // WHEN showBouncer is called - mStatusBarKeyguardViewManager.showPrimaryBouncer(true); - - // THEN alt bouncer should be hidden - verify(mAlternateBouncer).hideAlternateBouncer(); - } - - @Test public void testBouncerIsOrWillBeShowing_whenBouncerIsInTransit() { when(mPrimaryBouncer.isShowing()).thenReturn(false); when(mPrimaryBouncer.inTransit()).thenReturn(true); @@ -484,38 +451,6 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { } @Test - public void testShowAltAuth_unlockingWithBiometricNotAllowed() { - // GIVEN alt auth exists, unlocking with biometric isn't allowed - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) - .thenReturn(false); - - // WHEN showGenericBouncer is called - final boolean scrimmed = true; - mStatusBarKeyguardViewManager.showBouncer(scrimmed); - - // THEN regular bouncer is shown - verify(mPrimaryBouncer).show(anyBoolean(), eq(scrimmed)); - verify(mAlternateBouncer, never()).showAlternateBouncer(); - } - - @Test - public void testShowAlternateBouncer_unlockingWithBiometricAllowed() { - // GIVEN alt auth exists, unlocking with biometric is allowed - mStatusBarKeyguardViewManager.setAlternateBouncer(mAlternateBouncer); - when(mPrimaryBouncer.isShowing()).thenReturn(false); - when(mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); - - // WHEN showGenericBouncer is called - mStatusBarKeyguardViewManager.showBouncer(true); - - // THEN alt auth bouncer is shown - verify(mAlternateBouncer).showAlternateBouncer(); - verify(mPrimaryBouncer, never()).show(anyBoolean(), anyBoolean()); - } - - @Test public void testUpdateResources_delegatesToBouncer() { mStatusBarKeyguardViewManager.updateResources(); @@ -628,7 +563,8 @@ public class StatusBarKeyguardViewManagerTest_Old extends SysuiTestCase { mFeatureFlags, mPrimaryBouncerCallbackInteractor, mPrimaryBouncerInteractor, - mBouncerView) { + mBouncerView, + mAlternateBouncerInteractor) { @Override public ViewRootImpl getViewRootImpl() { return mViewRootImpl; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt index 5d377a8658a5..0859d140c3b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -34,6 +34,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.valid import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock @@ -71,8 +73,10 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { private lateinit var underTest: MobileRepositorySwitcher private lateinit var realRepo: MobileConnectionsRepositoryImpl private lateinit var demoRepo: DemoMobileConnectionsRepository - private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource + private lateinit var wifiDataSource: DemoModeWifiDataSource private lateinit var logFactory: TableLogBufferFactory + private lateinit var wifiRepository: FakeWifiRepository @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @@ -96,10 +100,15 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { // Never start in demo mode whenever(demoModeController.isInDemoMode).thenReturn(false) - mockDataSource = + mobileDataSource = mock<DemoModeMobileConnectionDataSource>().also { whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow) } + wifiDataSource = + mock<DemoModeWifiDataSource>().also { + whenever(it.wifiEvents).thenReturn(MutableStateFlow(null)) + } + wifiRepository = FakeWifiRepository() realRepo = MobileConnectionsRepositoryImpl( @@ -113,12 +122,14 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { context, IMMEDIATE, scope, + wifiRepository, mock(), ) demoRepo = DemoMobileConnectionsRepository( - dataSource = mockDataSource, + mobileDataSource = mobileDataSource, + wifiDataSource = wifiDataSource, scope = scope, context = context, logFactory = logFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt index 210208532dd4..6989b514a703 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt @@ -29,6 +29,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectio import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock @@ -63,10 +65,12 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC private val testScope = TestScope(testDispatcher) private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null) + private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null) private lateinit var connectionsRepo: DemoMobileConnectionsRepository private lateinit var underTest: DemoMobileConnectionRepository private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var mockWifiDataSource: DemoModeWifiDataSource @Before fun setUp() { @@ -75,10 +79,15 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC mock<DemoModeMobileConnectionDataSource>().also { whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow) } + mockWifiDataSource = + mock<DemoModeWifiDataSource>().also { + whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow) + } connectionsRepo = DemoMobileConnectionsRepository( - dataSource = mockDataSource, + mobileDataSource = mockDataSource, + wifiDataSource = mockWifiDataSource, scope = testScope.backgroundScope, context = context, logFactory = logFactory, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt index cdbe75e855bc..9d16b7fe5246 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt @@ -32,6 +32,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionMod import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock @@ -57,21 +59,28 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { private val testScope = TestScope(testDispatcher) private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null) + private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null) private lateinit var underTest: DemoMobileConnectionsRepository - private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource + private lateinit var wifiDataSource: DemoModeWifiDataSource @Before fun setUp() { // The data source only provides one API, so we can mock it with a flow here for convenience - mockDataSource = + mobileDataSource = mock<DemoModeMobileConnectionDataSource>().also { whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow) } + wifiDataSource = + mock<DemoModeWifiDataSource>().also { + whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow) + } underTest = DemoMobileConnectionsRepository( - dataSource = mockDataSource, + mobileDataSource = mobileDataSource, + wifiDataSource = wifiDataSource, scope = testScope.backgroundScope, context = context, logFactory = logFactory, @@ -97,6 +106,22 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `wifi carrier merged event - create new subscription`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEmpty() + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(5) + + job.cancel() + } + + @Test fun `network event - reuses subscription when same Id`() = testScope.runTest { var latest: List<SubscriptionModel>? = null @@ -119,6 +144,28 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `wifi carrier merged event - reuses subscription when same Id`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEmpty() + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(5) + + // Second network event comes in with the same subId, does not create a new subscription + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 2) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].subscriptionId).isEqualTo(5) + + job.cancel() + } + + @Test fun `multiple subscriptions`() = testScope.runTest { var latest: List<SubscriptionModel>? = null @@ -133,6 +180,35 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `mobile subscription and carrier merged subscription`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5) + + assertThat(latest).hasSize(2) + + job.cancel() + } + + @Test + fun `multiple mobile subscriptions and carrier merged subscription`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 1) + fakeNetworkEventFlow.value = validMobileEvent(subId = 2) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3) + + assertThat(latest).hasSize(3) + + job.cancel() + } + + @Test fun `mobile disabled event - disables connection - subId specified - single conn`() = testScope.runTest { var latest: List<SubscriptionModel>? = null @@ -194,6 +270,112 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun `wifi network updates to disabled - carrier merged connection removed`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1) + + assertThat(latest).hasSize(1) + + fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled + + assertThat(latest).isEmpty() + + job.cancel() + } + + @Test + fun `wifi network updates to active - carrier merged connection removed`() = + testScope.runTest { + var latest: List<SubscriptionModel>? = null + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1) + + assertThat(latest).hasSize(1) + + fakeWifiEventFlow.value = + FakeWifiEventModel.Wifi( + level = 1, + activity = 0, + ssid = null, + validated = true, + ) + + assertThat(latest).isEmpty() + + job.cancel() + } + + @Test + fun `mobile sub updates to carrier merged - only one connection`() = + testScope.runTest { + var latestSubsList: List<SubscriptionModel>? = null + var connections: List<DemoMobileConnectionRepository>? = null + val job = + underTest.subscriptions + .onEach { latestSubsList = it } + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + fakeNetworkEventFlow.value = validMobileEvent(subId = 3, level = 2) + assertThat(latestSubsList).hasSize(1) + + val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1) + fakeWifiEventFlow.value = carrierMergedEvent + assertThat(latestSubsList).hasSize(1) + val connection = connections!!.find { it.subId == 3 }!! + assertCarrierMergedConnection(connection, carrierMergedEvent) + + job.cancel() + } + + @Test + fun `mobile sub updates to carrier merged then back - has old mobile data`() = + testScope.runTest { + var latestSubsList: List<SubscriptionModel>? = null + var connections: List<DemoMobileConnectionRepository>? = null + val job = + underTest.subscriptions + .onEach { latestSubsList = it } + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + val mobileEvent = validMobileEvent(subId = 3, level = 2) + fakeNetworkEventFlow.value = mobileEvent + assertThat(latestSubsList).hasSize(1) + + val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1) + fakeWifiEventFlow.value = carrierMergedEvent + assertThat(latestSubsList).hasSize(1) + var connection = connections!!.find { it.subId == 3 }!! + assertCarrierMergedConnection(connection, carrierMergedEvent) + + // WHEN the carrier merged is removed + fakeWifiEventFlow.value = + FakeWifiEventModel.Wifi( + level = 4, + activity = 0, + ssid = null, + validated = true, + ) + + // THEN the subId=3 connection goes back to the mobile information + connection = connections!!.find { it.subId == 3 }!! + assertConnection(connection, mobileEvent) + + job.cancel() + } + /** Regression test for b/261706421 */ @Test fun `multiple connections - remove all - does not throw`() = @@ -289,6 +471,51 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun `demo connection - two connections - update carrier merged - no affect on first`() = + testScope.runTest { + var currentEvent1 = validMobileEvent(subId = 1) + var connection1: DemoMobileConnectionRepository? = null + var currentEvent2 = validCarrierMergedEvent(subId = 2) + var connection2: DemoMobileConnectionRepository? = null + var connections: List<DemoMobileConnectionRepository>? = null + val job = + underTest.subscriptions + .onEach { infos -> + connections = + infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) } + } + .launchIn(this) + + fakeNetworkEventFlow.value = currentEvent1 + fakeWifiEventFlow.value = currentEvent2 + assertThat(connections).hasSize(2) + connections!!.forEach { + when (it.subId) { + 1 -> connection1 = it + 2 -> connection2 = it + else -> Assert.fail("Unexpected subscription") + } + } + + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) + + // WHEN the event changes for connection 2, it updates, and connection 1 stays the same + currentEvent2 = validCarrierMergedEvent(subId = 2, level = 4) + fakeWifiEventFlow.value = currentEvent2 + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) + + // and vice versa + currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true) + fakeNetworkEventFlow.value = currentEvent1 + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) + + job.cancel() + } + private fun assertConnection( conn: DemoMobileConnectionRepository, model: FakeNetworkEventModel @@ -315,6 +542,21 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { else -> {} } } + + private fun assertCarrierMergedConnection( + conn: DemoMobileConnectionRepository, + model: FakeWifiEventModel.CarrierMerged, + ) { + val connectionInfo: MobileConnectionModel = conn.connectionInfo.value + assertThat(conn.subId).isEqualTo(model.subscriptionId) + assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) + assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) + assertThat(connectionInfo.carrierNetworkChangeActive).isEqualTo(false) + assertThat(connectionInfo.isRoaming).isEqualTo(false) + assertThat(connectionInfo.isEmergencyOnly).isFalse() + assertThat(connectionInfo.isGsm).isFalse() + assertThat(connectionInfo.dataConnectionState).isEqualTo(DataConnectionState.Connected) + } } /** Convenience to create a valid fake network event with minimal params */ @@ -339,3 +581,14 @@ fun validMobileEvent( roaming = roaming, name = "demo name", ) + +fun validCarrierMergedEvent( + subId: Int = 1, + level: Int = 1, + numberOfLevels: Int = 4, +): FakeWifiEventModel.CarrierMerged = + FakeWifiEventModel.CarrierMerged( + subscriptionId = subId, + level = level, + numberOfLevels = numberOfLevels, + ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt new file mode 100644 index 000000000000..ea90150b432a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt @@ -0,0 +1,251 @@ +/* + * 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.statusbar.pipeline.mobile.data.repository.prod + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +class CarrierMergedConnectionRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: CarrierMergedConnectionRepository + + private lateinit var wifiRepository: FakeWifiRepository + @Mock private lateinit var logger: TableLogBuffer + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + wifiRepository = FakeWifiRepository() + + underTest = + CarrierMergedConnectionRepository( + SUB_ID, + logger, + NetworkNameModel.Default("name"), + testScope.backgroundScope, + wifiRepository, + ) + } + + @Test + fun connectionInfo_inactiveWifi_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + @Test + fun connectionInfo_activeWifi_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = NET_ID, level = 1)) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + @Test + fun connectionInfo_carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setIsWifiEnabled(true) + wifiRepository.setIsWifiDefault(true) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 3, + ) + ) + + val expected = + MobileConnectionModel( + primaryLevel = 3, + cdmaLevel = 3, + dataConnectionState = DataConnectionState.Connected, + dataActivityDirection = + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ), + resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType, + isRoaming = false, + isEmergencyOnly = false, + operatorAlphaShort = null, + isInService = true, + isGsm = false, + carrierNetworkChangeActive = false, + ) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun connectionInfo_carrierMergedWifi_wrongSubId_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID + 10, + level = 3, + ) + ) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + assertThat(latest!!.primaryLevel).isNotEqualTo(3) + assertThat(latest!!.resolvedNetworkType) + .isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) + + job.cancel() + } + + // This scenario likely isn't possible, but write a test for it anyway + @Test + fun connectionInfo_carrierMergedButNotEnabled_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 3, + ) + ) + wifiRepository.setIsWifiEnabled(false) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + // This scenario likely isn't possible, but write a test for it anyway + @Test + fun connectionInfo_carrierMergedButWifiNotDefault_isDefault() = + testScope.runTest { + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 3, + ) + ) + wifiRepository.setIsWifiDefault(false) + + assertThat(latest).isEqualTo(MobileConnectionModel()) + + job.cancel() + } + + @Test + fun numberOfLevels_comesFromCarrierMerged() = + testScope.runTest { + var latest: Int? = null + val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged( + networkId = NET_ID, + subscriptionId = SUB_ID, + level = 1, + numberOfLevels = 6, + ) + ) + + assertThat(latest).isEqualTo(6) + + job.cancel() + } + + @Test + fun dataEnabled_matchesWifiEnabled() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + + wifiRepository.setIsWifiEnabled(true) + assertThat(latest).isTrue() + + wifiRepository.setIsWifiEnabled(false) + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun cdmaRoaming_alwaysFalse() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + private companion object { + const val SUB_ID = 123 + const val NET_ID = 456 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt new file mode 100644 index 000000000000..c02a4dfd074c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +/** + * This repo acts as a dispatcher to either the `typical` or `carrier merged` versions of the + * repository interface it's switching on. These tests just need to verify that the entire interface + * properly switches over when the value of `isCarrierMerged` changes. + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class FullMobileConnectionRepositoryTest : SysuiTestCase() { + private lateinit var underTest: FullMobileConnectionRepository + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val mobileMappings = FakeMobileMappingsProxy() + private val tableLogBuffer = mock<TableLogBuffer>() + private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>() + private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>() + + private lateinit var connectionsRepo: FakeMobileConnectionsRepository + private val globalMobileDataSettingChangedEvent: Flow<Unit> + get() = connectionsRepo.globalMobileDataSettingChangedEvent + + private lateinit var mobileRepo: FakeMobileConnectionRepository + private lateinit var carrierMergedRepo: FakeMobileConnectionRepository + + @Before + fun setUp() { + connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogBuffer) + + mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer) + carrierMergedRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer) + + whenever( + mobileFactory.build( + eq(SUB_ID), + any(), + eq(DEFAULT_NAME), + eq(SEP), + eq(globalMobileDataSettingChangedEvent), + ) + ) + .thenReturn(mobileRepo) + whenever(carrierMergedFactory.build(eq(SUB_ID), any(), eq(DEFAULT_NAME))) + .thenReturn(carrierMergedRepo) + } + + @Test + fun startingIsCarrierMerged_usesCarrierMergedInitially() = + testScope.runTest { + val carrierMergedConnectionInfo = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator", + ) + carrierMergedRepo.setConnectionInfo(carrierMergedConnectionInfo) + + initializeRepo(startingIsCarrierMerged = true) + + assertThat(underTest.activeRepo.value).isEqualTo(carrierMergedRepo) + assertThat(underTest.connectionInfo.value).isEqualTo(carrierMergedConnectionInfo) + verify(mobileFactory, never()) + .build( + SUB_ID, + tableLogBuffer, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent + ) + } + + @Test + fun startingNotCarrierMerged_usesTypicalInitially() = + testScope.runTest { + val mobileConnectionInfo = + MobileConnectionModel( + operatorAlphaShort = "Typical Operator", + ) + mobileRepo.setConnectionInfo(mobileConnectionInfo) + + initializeRepo(startingIsCarrierMerged = false) + + assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo) + assertThat(underTest.connectionInfo.value).isEqualTo(mobileConnectionInfo) + verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer, DEFAULT_NAME) + } + + @Test + fun activeRepo_matchesIsCarrierMerged() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + var latest: MobileConnectionRepository? = null + val job = underTest.activeRepo.onEach { latest = it }.launchIn(this) + + underTest.setIsCarrierMerged(true) + + assertThat(latest).isEqualTo(carrierMergedRepo) + + underTest.setIsCarrierMerged(false) + + assertThat(latest).isEqualTo(mobileRepo) + + underTest.setIsCarrierMerged(true) + + assertThat(latest).isEqualTo(carrierMergedRepo) + + job.cancel() + } + + @Test + fun connectionInfo_getsUpdatesFromRepo_carrierMerged() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + underTest.setIsCarrierMerged(true) + + val info1 = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator", + primaryLevel = 1, + ) + carrierMergedRepo.setConnectionInfo(info1) + + assertThat(latest).isEqualTo(info1) + + val info2 = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator #2", + primaryLevel = 2, + ) + carrierMergedRepo.setConnectionInfo(info2) + + assertThat(latest).isEqualTo(info2) + + val info3 = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator #3", + primaryLevel = 3, + ) + carrierMergedRepo.setConnectionInfo(info3) + + assertThat(latest).isEqualTo(info3) + + job.cancel() + } + + @Test + fun connectionInfo_getsUpdatesFromRepo_mobile() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + underTest.setIsCarrierMerged(false) + + val info1 = + MobileConnectionModel( + operatorAlphaShort = "Typical Merged Operator", + primaryLevel = 1, + ) + mobileRepo.setConnectionInfo(info1) + + assertThat(latest).isEqualTo(info1) + + val info2 = + MobileConnectionModel( + operatorAlphaShort = "Typical Merged Operator #2", + primaryLevel = 2, + ) + mobileRepo.setConnectionInfo(info2) + + assertThat(latest).isEqualTo(info2) + + val info3 = + MobileConnectionModel( + operatorAlphaShort = "Typical Merged Operator #3", + primaryLevel = 3, + ) + mobileRepo.setConnectionInfo(info3) + + assertThat(latest).isEqualTo(info3) + + job.cancel() + } + + @Test + fun connectionInfo_updatesWhenCarrierMergedUpdates() = + testScope.runTest { + initializeRepo(startingIsCarrierMerged = false) + + var latest: MobileConnectionModel? = null + val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) + + val carrierMergedInfo = + MobileConnectionModel( + operatorAlphaShort = "Carrier Merged Operator", + primaryLevel = 4, + ) + carrierMergedRepo.setConnectionInfo(carrierMergedInfo) + + val mobileInfo = + MobileConnectionModel( + operatorAlphaShort = "Typical Operator", + primaryLevel = 2, + ) + mobileRepo.setConnectionInfo(mobileInfo) + + // Start with the mobile info + assertThat(latest).isEqualTo(mobileInfo) + + // WHEN isCarrierMerged is set to true + underTest.setIsCarrierMerged(true) + + // THEN the carrier merged info is used + assertThat(latest).isEqualTo(carrierMergedInfo) + + val newCarrierMergedInfo = + MobileConnectionModel( + operatorAlphaShort = "New CM Operator", + primaryLevel = 0, + ) + carrierMergedRepo.setConnectionInfo(newCarrierMergedInfo) + + assertThat(latest).isEqualTo(newCarrierMergedInfo) + + // WHEN isCarrierMerged is set to false + underTest.setIsCarrierMerged(false) + + // THEN the typical info is used + assertThat(latest).isEqualTo(mobileInfo) + + val newMobileInfo = + MobileConnectionModel( + operatorAlphaShort = "New Mobile Operator", + primaryLevel = 3, + ) + mobileRepo.setConnectionInfo(newMobileInfo) + + assertThat(latest).isEqualTo(newMobileInfo) + + job.cancel() + } + + @Test + fun `factory - reuses log buffers for same connection`() = + testScope.runTest { + val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock()) + + val factory = + FullMobileConnectionRepository.Factory( + scope = testScope.backgroundScope, + realLoggerFactory, + mobileFactory, + carrierMergedFactory, + ) + + // Create two connections for the same subId. Similar to if the connection appeared + // and disappeared from the connectionFactory's perspective + val connection1 = + factory.build( + SUB_ID, + startingIsCarrierMerged = false, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + val connection1Repeat = + factory.build( + SUB_ID, + startingIsCarrierMerged = false, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + assertThat(connection1.tableLogBuffer) + .isSameInstanceAs(connection1Repeat.tableLogBuffer) + } + + @Test + fun `factory - reuses log buffers for same sub ID even if carrier merged`() = + testScope.runTest { + val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock()) + + val factory = + FullMobileConnectionRepository.Factory( + scope = testScope.backgroundScope, + realLoggerFactory, + mobileFactory, + carrierMergedFactory, + ) + + val connection1 = + factory.build( + SUB_ID, + startingIsCarrierMerged = false, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + // WHEN a connection with the same sub ID but carrierMerged = true is created + val connection1Repeat = + factory.build( + SUB_ID, + startingIsCarrierMerged = true, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + ) + + // THEN the same table is re-used + assertThat(connection1.tableLogBuffer) + .isSameInstanceAs(connection1Repeat.tableLogBuffer) + } + + // TODO(b/238425913): Verify that the logging switches correctly (once the carrier merged repo + // implements logging). + + private fun initializeRepo(startingIsCarrierMerged: Boolean) { + underTest = + FullMobileConnectionRepository( + SUB_ID, + startingIsCarrierMerged, + tableLogBuffer, + DEFAULT_NAME, + SEP, + globalMobileDataSettingChangedEvent, + testScope.backgroundScope, + mobileFactory, + carrierMergedFactory, + ) + } + + private companion object { + const val SUB_ID = 42 + private val DEFAULT_NAME = NetworkNameModel.Default("default name") + private const val SEP = "-" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 0da15e239932..813b0ed041a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -38,8 +38,11 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq @@ -72,6 +75,9 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private lateinit var underTest: MobileConnectionsRepositoryImpl private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory + private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory + private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory + private lateinit var wifiRepository: FakeWifiRepository @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @@ -94,10 +100,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } } - whenever(logBufferFactory.create(anyString(), anyInt())).thenAnswer { _ -> + whenever(logBufferFactory.getOrCreate(anyString(), anyInt())).thenAnswer { _ -> mock<TableLogBuffer>() } + wifiRepository = FakeWifiRepository() + connectionFactory = MobileConnectionRepositoryImpl.Factory( fakeBroadcastDispatcher, @@ -108,7 +116,18 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { logger = logger, mobileMappingsProxy = mobileMappings, scope = scope, + ) + carrierMergedFactory = + CarrierMergedConnectionRepository.Factory( + scope, + wifiRepository, + ) + fullConnectionFactory = + FullMobileConnectionRepository.Factory( + scope = scope, logFactory = logBufferFactory, + mobileRepoFactory = connectionFactory, + carrierMergedRepoFactory = carrierMergedFactory, ) underTest = @@ -123,7 +142,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { context, IMMEDIATE, scope, - connectionFactory, + wifiRepository, + fullConnectionFactory, ) } @@ -178,6 +198,40 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionModel>? = null + + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(MODEL_CM)) + + job.cancel() + } + + @Test + fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionModel>? = null + + val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM)) + + job.cancel() + } + + @Test fun testActiveDataSubscriptionId_initialValueIsInvalidId() = runBlocking(IMMEDIATE) { assertThat(underTest.activeMobileDataSubscriptionId.value) @@ -217,6 +271,96 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun testConnectionRepository_carrierMergedSubId_isCached() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val repo1 = underTest.getRepoForSubId(SUB_CM_ID) + val repo2 = underTest.getRepoForSubId(SUB_CM_ID) + + assertThat(repo1).isSameInstanceAs(repo2) + + job.cancel() + } + + @Test + fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + val mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + job.cancel() + } + + @Test + fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + // WHEN the wifi network updates to be not carrier merged + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 4, level = 1)) + + // THEN the repos update + val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + job.cancel() + } + + @Test + fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + var mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(notYetCarrierMergedRepo.getIsCarrierMerged()).isFalse() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + // WHEN the wifi network updates to be carrier merged + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + + // THEN the repos update + val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID) + mobileRepo = underTest.getRepoForSubId(SUB_1_ID) + assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue() + assertThat(mobileRepo.getIsCarrierMerged()).isFalse() + + job.cancel() + } + + @Test fun testConnectionCache_clearsInvalidSubscriptions() = runBlocking(IMMEDIATE) { val job = underTest.subscriptions.launchIn(this) @@ -242,6 +386,34 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger caching + val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repo2 = underTest.getRepoForSubId(SUB_2_ID) + val repoCarrierMerged = underTest.getRepoForSubId(SUB_CM_ID) + + assertThat(underTest.getSubIdRepoCache()) + .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2, SUB_CM_ID, repoCarrierMerged) + + // SUB_2 and SUB_CM disappear + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) + + job.cancel() + } + /** Regression test for b/261706421 */ @Test fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() = @@ -292,14 +464,14 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // Get repos to trigger creation underTest.getRepoForSubId(SUB_1_ID) verify(logBufferFactory) - .create( - eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)), + .getOrCreate( + eq(tableBufferLogName(SUB_1_ID)), anyInt(), ) underTest.getRepoForSubId(SUB_2_ID) verify(logBufferFactory) - .create( - eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)), + .getOrCreate( + eq(tableBufferLogName(SUB_2_ID)), anyInt(), ) @@ -419,7 +591,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { context, IMMEDIATE, scope, - connectionFactory, + wifiRepository, + fullConnectionFactory, ) var latest: MobileMappings.Config? = null @@ -529,5 +702,16 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private const val NET_ID = 123 private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) } + + private const val SUB_CM_ID = 5 + private val SUB_CM = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) } + private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID) + private val WIFI_NETWORK_CM = + WifiNetworkModel.CarrierMerged( + networkId = 3, + subscriptionId = SUB_CM_ID, + level = 1, + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 61e13b85db6c..e6be7f15235b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository @@ -271,6 +272,23 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test + fun iconGroup_carrierMerged_usesOverride() = + runBlocking(IMMEDIATE) { + connectionRepository.setConnectionInfo( + MobileConnectionModel( + resolvedNetworkType = CarrierMergedNetworkType, + ), + ) + + var latest: MobileIconGroup? = null + val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(CarrierMergedNetworkType.iconGroupOverride) + + job.cancel() + } + + @Test fun alwaysShowDataRatIcon_matchesParent() = runBlocking(IMMEDIATE) { var latest: Boolean? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt index 30ac8d432e8a..824cebdc3c08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt @@ -16,11 +16,12 @@ package com.android.systemui.statusbar.pipeline.wifi.data.model +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableRowLogger import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL -import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MIN_VALID_LEVEL +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Companion.MIN_VALID_LEVEL import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -44,9 +45,53 @@ class WifiNetworkModelTest : SysuiTestCase() { WifiNetworkModel.Active(NETWORK_ID, level = MAX_VALID_LEVEL + 1) } + @Test(expected = IllegalArgumentException::class) + fun carrierMerged_invalidSubId_exceptionThrown() { + WifiNetworkModel.CarrierMerged(NETWORK_ID, INVALID_SUBSCRIPTION_ID, 1) + } + // Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken @Test + fun logDiffs_carrierMergedToInactive_resetsAllFields() { + val logger = TestLogger() + val prevVal = + WifiNetworkModel.CarrierMerged( + networkId = 5, + subscriptionId = 3, + level = 1, + ) + + WifiNetworkModel.Inactive.logDiffs(prevVal, logger) + + assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE)) + assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString())) + assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false")) + assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString())) + assertThat(logger.changes).contains(Pair(COL_SSID, "null")) + } + + @Test + fun logDiffs_inactiveToCarrierMerged_logsAllFields() { + val logger = TestLogger() + val carrierMerged = + WifiNetworkModel.CarrierMerged( + networkId = 6, + subscriptionId = 3, + level = 2, + ) + + carrierMerged.logDiffs(prevVal = WifiNetworkModel.Inactive, logger) + + assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)) + assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6")) + assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3")) + assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true")) + assertThat(logger.changes).contains(Pair(COL_LEVEL, "2")) + assertThat(logger.changes).contains(Pair(COL_SSID, "null")) + } + + @Test fun logDiffs_inactiveToActive_logsAllActiveFields() { val logger = TestLogger() val activeNetwork = @@ -95,8 +140,14 @@ class WifiNetworkModelTest : SysuiTestCase() { level = 3, ssid = "Test SSID" ) + val prevVal = + WifiNetworkModel.CarrierMerged( + networkId = 5, + subscriptionId = 3, + level = 1, + ) - activeNetwork.logDiffs(prevVal = WifiNetworkModel.CarrierMerged, logger) + activeNetwork.logDiffs(prevVal, logger) assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE)) assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "5")) @@ -105,7 +156,7 @@ class WifiNetworkModelTest : SysuiTestCase() { assertThat(logger.changes).contains(Pair(COL_SSID, "Test SSID")) } @Test - fun logDiffs_activeToCarrierMerged_resetsAllActiveFields() { + fun logDiffs_activeToCarrierMerged_logsAllFields() { val logger = TestLogger() val activeNetwork = WifiNetworkModel.Active( @@ -114,13 +165,20 @@ class WifiNetworkModelTest : SysuiTestCase() { level = 3, ssid = "Test SSID" ) + val carrierMerged = + WifiNetworkModel.CarrierMerged( + networkId = 6, + subscriptionId = 3, + level = 2, + ) - WifiNetworkModel.CarrierMerged.logDiffs(prevVal = activeNetwork, logger) + carrierMerged.logDiffs(prevVal = activeNetwork, logger) assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED)) - assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, NETWORK_ID_DEFAULT.toString())) - assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false")) - assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString())) + assertThat(logger.changes).contains(Pair(COL_NETWORK_ID, "6")) + assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3")) + assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true")) + assertThat(logger.changes).contains(Pair(COL_LEVEL, "2")) assertThat(logger.changes).contains(Pair(COL_SSID, "null")) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt index 8f07615b19b2..87ce8faff5a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt @@ -26,6 +26,7 @@ import android.net.vcn.VcnTransportInfo import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher @@ -340,7 +341,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { .launchIn(this) val wifiInfo = mock<WifiInfo>().apply { - whenever(this.ssid).thenReturn(SSID) whenever(this.isPrimary).thenReturn(true) whenever(this.isCarrierMerged).thenReturn(true) } @@ -353,6 +353,67 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test + fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() = + runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val wifiInfo = mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID) + } + + getNetworkCallback().onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo), + ) + + assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java) + + job.cancel() + } + + @Test + fun wifiNetwork_isCarrierMerged_getsCorrectValues() = + runBlocking(IMMEDIATE) { + var latest: WifiNetworkModel? = null + val job = underTest + .wifiNetwork + .onEach { latest = it } + .launchIn(this) + + val rssi = -57 + val wifiInfo = mock<WifiInfo>().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.rssi).thenReturn(rssi) + whenever(this.subscriptionId).thenReturn(567) + } + + whenever(wifiManager.calculateSignalLevel(rssi)).thenReturn(2) + whenever(wifiManager.maxSignalLevel).thenReturn(5) + + getNetworkCallback().onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo), + ) + + assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() + val latestCarrierMerged = latest as WifiNetworkModel.CarrierMerged + assertThat(latestCarrierMerged.networkId).isEqualTo(NETWORK_ID) + assertThat(latestCarrierMerged.subscriptionId).isEqualTo(567) + assertThat(latestCarrierMerged.level).isEqualTo(2) + // numberOfLevels = maxSignalLevel + 1 + assertThat(latestCarrierMerged.numberOfLevels).isEqualTo(6) + + job.cancel() + } + + @Test fun wifiNetwork_notValidated_networkNotValidated() = runBlocking(IMMEDIATE) { var latest: WifiNetworkModel? = null val job = underTest diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt index 01d59f96c221..089a170aa2be 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt @@ -84,7 +84,9 @@ class WifiInteractorImplTest : SysuiTestCase() { @Test fun ssid_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) { - wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged) + wifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged(networkId = 1, subscriptionId = 2, level = 1) + ) var latest: String? = "default" val job = underTest diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt index 726e813ec414..b9328377772a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -206,7 +206,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // Enabled = false => no networks shown TestCase( enabled = false, - network = WifiNetworkModel.CarrierMerged, + network = + WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1), expected = null, ), TestCase( @@ -228,7 +229,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // forceHidden = true => no networks shown TestCase( forceHidden = true, - network = WifiNetworkModel.CarrierMerged, + network = + WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1), expected = null, ), TestCase( @@ -369,7 +371,8 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase // network = CarrierMerged => not shown TestCase( - network = WifiNetworkModel.CarrierMerged, + network = + WifiNetworkModel.CarrierMerged(NETWORK_ID, subscriptionId = 1, level = 1), expected = null, ), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 4b32ee262cdc..0cca7b2aa38c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -390,19 +390,27 @@ public class RemoteInputViewTest extends SysuiTestCase { bindController(view, row.getEntry()); view.setVisibility(View.GONE); - View crossFadeView = new View(mContext); + View fadeOutView = new View(mContext); + fadeOutView.setId(com.android.internal.R.id.actions_container_layout); - // Start focus animation - view.focusAnimated(crossFadeView); + FrameLayout parent = new FrameLayout(mContext); + parent.addView(view); + parent.addView(fadeOutView); + // Start focus animation + view.focusAnimated(); assertTrue(view.isAnimatingAppearance()); + // fast forward to 1 ms before end of animation and verify fadeOutView has alpha set to 0f + mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD - 1); + assertEquals(0f, fadeOutView.getAlpha()); + // fast forward to end of animation - mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD); + mAnimatorTestRule.advanceTimeBy(1); - // assert that crossFadeView's alpha is reset to 1f after the animation (hidden behind + // assert that fadeOutView's alpha is reset to 1f after the animation (hidden behind // RemoteInputView) - assertEquals(1f, crossFadeView.getAlpha()); + assertEquals(1f, fadeOutView.getAlpha()); assertFalse(view.isAnimatingAppearance()); assertEquals(View.VISIBLE, view.getVisibility()); assertEquals(1f, view.getAlpha()); @@ -415,20 +423,27 @@ public class RemoteInputViewTest extends SysuiTestCase { mDependency, TestableLooper.get(this)); ExpandableNotificationRow row = helper.createRow(); - FrameLayout remoteInputViewParent = new FrameLayout(mContext); RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); - remoteInputViewParent.addView(view); bindController(view, row.getEntry()); + View fadeInView = new View(mContext); + fadeInView.setId(com.android.internal.R.id.actions_container_layout); + + FrameLayout parent = new FrameLayout(mContext); + parent.addView(view); + parent.addView(fadeInView); + // Start defocus animation - view.onDefocus(true, false); + view.onDefocus(true /* animate */, false /* logClose */, null /* doAfterDefocus */); assertEquals(View.VISIBLE, view.getVisibility()); + assertEquals(0f, fadeInView.getAlpha()); // fast forward to end of animation mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD); // assert that RemoteInputView is no longer visible assertEquals(View.GONE, view.getVisibility()); + assertEquals(1f, fadeInView.getAlpha()); } // NOTE: because we're refactoring the RemoteInputView and moving logic into the diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt new file mode 100644 index 000000000000..7e010886c394 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/FixedCapacityBatteryState.kt @@ -0,0 +1,25 @@ +/* + * 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.stylus + +import android.hardware.BatteryState + +class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() { + override fun getCapacity() = capacity + override fun getStatus() = 0 + override fun isPresent() = true +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt deleted file mode 100644 index 8dd088f5760c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt +++ /dev/null @@ -1,289 +0,0 @@ -/* - * 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.stylus - -import android.content.Context -import android.hardware.BatteryState -import android.hardware.input.InputManager -import android.os.Handler -import android.testing.AndroidTestingRunner -import android.view.InputDevice -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.time.FakeSystemClock -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@SmallTest -@Ignore("TODO(b/20579491): unignore on main") -class StylusFirstUsageListenerTest : SysuiTestCase() { - @Mock lateinit var context: Context - @Mock lateinit var inputManager: InputManager - @Mock lateinit var stylusManager: StylusManager - @Mock lateinit var featureFlags: FeatureFlags - @Mock lateinit var internalStylusDevice: InputDevice - @Mock lateinit var otherDevice: InputDevice - @Mock lateinit var externalStylusDevice: InputDevice - @Mock lateinit var batteryState: BatteryState - @Mock lateinit var handler: Handler - - private lateinit var stylusListener: StylusFirstUsageListener - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true) - whenever(inputManager.isStylusEverUsed(context)).thenReturn(false) - - stylusListener = - StylusFirstUsageListener( - context, - inputManager, - stylusManager, - featureFlags, - EXECUTOR, - handler - ) - stylusListener.hasStarted = false - - whenever(handler.post(any())).thenAnswer { - (it.arguments[0] as Runnable).run() - true - } - - whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false) - whenever(internalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) - whenever(internalStylusDevice.isExternal).thenReturn(false) - whenever(externalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) - whenever(externalStylusDevice.isExternal).thenReturn(true) - - whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf()) - whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice) - whenever(inputManager.getInputDevice(INTERNAL_STYLUS_DEVICE_ID)) - .thenReturn(internalStylusDevice) - whenever(inputManager.getInputDevice(EXTERNAL_STYLUS_DEVICE_ID)) - .thenReturn(externalStylusDevice) - } - - @Test - fun start_flagDisabled_doesNotRegister() { - whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(false) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_toggleHasStarted() { - stylusListener.start() - - assert(stylusListener.hasStarted) - } - - @Test - fun start_hasStarted_doesNotRegister() { - stylusListener.hasStarted = true - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - } - - @Test - fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() { - whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(OTHER_DEVICE_ID)) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_stylusEverUsed_doesNotRegister() { - whenever(inputManager.inputDeviceIds) - .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID)) - whenever(inputManager.isStylusEverUsed(context)).thenReturn(true) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_hostDeviceSupportsStylus_registersListener() { - whenever(inputManager.inputDeviceIds) - .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID)) - - stylusListener.start() - - verify(stylusManager).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun onStylusAdded_hasNotStarted_doesNotRegisterListener() { - stylusListener.hasStarted = false - - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - verifyZeroInteractions(inputManager) - } - - @Test - fun onStylusAdded_internalStylus_registersListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, EXECUTOR, stylusListener) - } - - @Test - fun onStylusAdded_externalStylus_doesNotRegisterListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusAdded(EXTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any()) - } - - @Test - fun onStylusAdded_otherDevice_doesNotRegisterListener() { - stylusListener.onStylusAdded(OTHER_DEVICE_ID) - - verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any()) - } - - @Test - fun onStylusRemoved_registeredDevice_unregistersListener() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - } - - @Test - fun onStylusRemoved_hasNotStarted_doesNotUnregisterListener() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verifyZeroInteractions(inputManager) - } - - @Test - fun onStylusRemoved_unregisteredDevice_doesNotUnregisterListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verifyNoMoreInteractions(inputManager) - } - - @Test - fun onStylusBluetoothConnected_updateStylusFlagAndUnregisters() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY") - - verify(inputManager).setStylusEverUsed(context, true) - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - verify(stylusManager).unregisterCallback(stylusListener) - } - - @Test - fun onStylusBluetoothConnected_hasNotStarted_doesNoting() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY") - - verifyZeroInteractions(inputManager) - verifyZeroInteractions(stylusManager) - } - - @Test - fun onBatteryStateChanged_batteryPresent_updateStylusFlagAndUnregisters() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(true) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verify(inputManager).setStylusEverUsed(context, true) - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - verify(stylusManager).unregisterCallback(stylusListener) - } - - @Test - fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateFlagOrUnregister() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(false) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verifyZeroInteractions(stylusManager) - verify(inputManager, never()) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - } - - @Test - fun onBatteryStateChanged_hasNotStarted_doesNothing() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(false) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verifyZeroInteractions(inputManager) - verifyZeroInteractions(stylusManager) - } - - companion object { - private const val OTHER_DEVICE_ID = 0 - private const val INTERNAL_STYLUS_DEVICE_ID = 1 - private const val EXTERNAL_STYLUS_DEVICE_ID = 2 - private val EXECUTOR = FakeExecutor(FakeSystemClock()) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt index 984de5b67bf5..6d6e40a90fa6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt @@ -17,12 +17,15 @@ package com.android.systemui.stylus import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.hardware.BatteryState import android.hardware.input.InputManager import android.os.Handler import android.testing.AndroidTestingRunner import android.view.InputDevice import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import java.util.concurrent.Executor @@ -31,30 +34,27 @@ import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.inOrder import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest -@Ignore("b/257936830 until bt APIs") class StylusManagerTest : SysuiTestCase() { @Mock lateinit var inputManager: InputManager - @Mock lateinit var stylusDevice: InputDevice - @Mock lateinit var btStylusDevice: InputDevice - @Mock lateinit var otherDevice: InputDevice - + @Mock lateinit var batteryState: BatteryState @Mock lateinit var bluetoothAdapter: BluetoothAdapter - @Mock lateinit var bluetoothDevice: BluetoothDevice - @Mock lateinit var handler: Handler + @Mock lateinit var featureFlags: FeatureFlags @Mock lateinit var stylusCallback: StylusManager.StylusCallback @@ -75,11 +75,8 @@ class StylusManagerTest : SysuiTestCase() { true } - stylusManager = StylusManager(inputManager, bluetoothAdapter, handler, EXECUTOR) - - stylusManager.registerCallback(stylusCallback) - - stylusManager.registerBatteryCallback(stylusBatteryCallback) + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false) whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) @@ -92,19 +89,47 @@ class StylusManagerTest : SysuiTestCase() { whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice) whenever(inputManager.getInputDevice(BT_STYLUS_DEVICE_ID)).thenReturn(btStylusDevice) whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(STYLUS_DEVICE_ID)) + whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(false) whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(bluetoothDevice) whenever(bluetoothDevice.address).thenReturn(STYLUS_BT_ADDRESS) + + whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true) + + stylusManager.startListener() + stylusManager.registerCallback(stylusCallback) + stylusManager.registerBatteryCallback(stylusBatteryCallback) + clearInvocations(inputManager) } @Test - fun startListener_registersInputDeviceListener() { + fun startListener_hasNotStarted_registersInputDeviceListener() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + stylusManager.startListener() verify(inputManager, times(1)).registerInputDeviceListener(any(), any()) } @Test + fun startListener_hasStarted_doesNothing() { + stylusManager.startListener() + + verifyZeroInteractions(inputManager) + } + + @Test + fun onInputDeviceAdded_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test fun onInputDeviceAdded_multipleRegisteredCallbacks_callsAll() { stylusManager.registerCallback(otherStylusCallback) @@ -117,6 +142,26 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceAdded_internalStylus_registersBatteryListener() { + whenever(stylusDevice.isExternal).thenReturn(false) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verify(inputManager, times(1)) + .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager) + } + + @Test + fun onInputDeviceAdded_externalStylus_doesNotRegisterbatteryListener() { + whenever(stylusDevice.isExternal).thenReturn(true) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verify(inputManager, never()) + .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager) + } + + @Test fun onInputDeviceAdded_stylus_callsCallbacksOnStylusAdded() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) @@ -125,6 +170,23 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") + fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() { + stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) + + verify(stylusCallback, times(1)).onStylusFirstUsed() + } + + @Test + @Ignore("b/257936830 until bt APIs") + fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() { + stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) + + verify(inputManager, times(1)).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -143,6 +205,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceChanged_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + + stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_multipleRegisteredCallbacks_callsAll() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS) @@ -157,6 +230,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_stylusNewBtConnection_callsCallbacks() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS) @@ -168,6 +242,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_stylusLostBtConnection_callsCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) // whenever(btStylusDevice.bluetoothAddress).thenReturn(null) @@ -179,6 +254,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -189,6 +265,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) @@ -198,6 +275,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceRemoved_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test fun onInputDeviceRemoved_multipleRegisteredCallbacks_callsAll() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) stylusManager.registerCallback(otherStylusCallback) @@ -219,6 +307,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceRemoved_unregistersBatteryListener() { + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID) + + verify(inputManager, times(1)) + .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceRemoved_btStylus_callsCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -232,6 +331,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothConnected_registersMetadataListener() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -239,6 +339,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() { whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null) @@ -248,6 +349,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothDisconnected_unregistersMetadataListener() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -257,6 +359,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) stylusManager.registerBatteryCallback(otherStylusBatteryCallback) @@ -274,6 +377,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -288,6 +392,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -302,6 +407,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() { stylusManager.onMetadataChanged( bluetoothDevice, @@ -313,6 +419,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -326,6 +433,63 @@ class StylusManagerTest : SysuiTestCase() { .onStylusBluetoothChargingStateChanged(any(), any(), any()) } + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() { + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() { + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(stylusCallback, times(1)).onStylusFirstUsed() + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() { + whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true) + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager, never()).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateEverUsedFlag() { + whenever(batteryState.isPresent).thenReturn(false) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager, never()) + .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager) + } + + @Test + fun onBatteryStateChanged_hasNotStarted_doesNothing() { + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verifyZeroInteractions(inputManager) + } + + @Test + fun onBatteryStateChanged_executesBatteryCallbacks() { + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(stylusBatteryCallback, times(1)) + .onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + } + companion object { private val EXECUTOR = Executor { r -> r.run() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt index ff382a3ec19f..1cccd65c8dbc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt @@ -25,17 +25,15 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.whenever -import java.util.concurrent.Executor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock -import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -60,7 +58,6 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { inputManager, stylusUsiPowerUi, featureFlags, - DIRECT_EXECUTOR, ) whenever(featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)).thenReturn(true) @@ -79,40 +76,19 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @Test - fun start_addsBatteryListenerForInternalStylus() { - startable.start() - - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) - } + fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() { + whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(EXTERNAL_DEVICE_ID)) - @Test - fun onStylusAdded_internalStylus_addsBatteryListener() { - startable.onStylusAdded(STYLUS_DEVICE_ID) + startable.start() - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) + verifyZeroInteractions(stylusManager) } @Test - fun onStylusAdded_externalStylus_doesNotAddBatteryListener() { - startable.onStylusAdded(EXTERNAL_DEVICE_ID) - - verify(inputManager, never()) - .addInputDeviceBatteryListener(EXTERNAL_DEVICE_ID, DIRECT_EXECUTOR, startable) - } + fun start_initStylusUsiPowerUi() { + startable.start() - @Test - fun onStylusRemoved_registeredStylus_removesBatteryListener() { - startable.onStylusAdded(STYLUS_DEVICE_ID) - startable.onStylusRemoved(STYLUS_DEVICE_ID) - - inOrder(inputManager).let { - it.verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) - it.verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, startable) - } + verify(stylusUsiPowerUi, times(1)).init() } @Test @@ -130,28 +106,34 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @Test - fun onBatteryStateChanged_batteryPresent_refreshesNotification() { - val batteryState = mock(BatteryState::class.java) - whenever(batteryState.isPresent).thenReturn(true) + fun onStylusUsiBatteryStateChanged_batteryPresentValidCapacity_refreshesNotification() { + val batteryState = FixedCapacityBatteryState(0.1f) + + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + + verify(stylusUsiPowerUi, times(1)).updateBatteryState(STYLUS_DEVICE_ID, batteryState) + } - startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + @Test + fun onStylusUsiBatteryStateChanged_batteryPresentInvalidCapacity_noop() { + val batteryState = FixedCapacityBatteryState(0f) + + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) - verify(stylusUsiPowerUi, times(1)).updateBatteryState(batteryState) + verifyNoMoreInteractions(stylusUsiPowerUi) } @Test - fun onBatteryStateChanged_batteryNotPresent_noop() { + fun onStylusUsiBatteryStateChanged_batteryNotPresent_noop() { val batteryState = mock(BatteryState::class.java) whenever(batteryState.isPresent).thenReturn(false) - startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) verifyNoMoreInteractions(stylusUsiPowerUi) } companion object { - private val DIRECT_EXECUTOR = Executor { r -> r.run() } - private const val EXTERNAL_DEVICE_ID = 0 private const val STYLUS_DEVICE_ID = 1 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt index 59875507341d..1e81dc761b18 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt @@ -16,8 +16,12 @@ package com.android.systemui.stylus -import android.hardware.BatteryState +import android.app.Notification +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent import android.hardware.input.InputManager +import android.os.Bundle import android.os.Handler import android.testing.AndroidTestingRunner import android.view.InputDevice @@ -26,14 +30,22 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import junit.framework.Assert.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.doNothing import org.mockito.Mockito.inOrder +import org.mockito.Mockito.never +import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions @@ -46,13 +58,19 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Mock lateinit var inputManager: InputManager @Mock lateinit var handler: Handler @Mock lateinit var btStylusDevice: InputDevice + @Captor lateinit var notificationCaptor: ArgumentCaptor<Notification> private lateinit var stylusUsiPowerUi: StylusUsiPowerUI + private lateinit var broadcastReceiver: BroadcastReceiver + private lateinit var contextSpy: Context @Before fun setUp() { MockitoAnnotations.initMocks(this) + contextSpy = spy(mContext) + doNothing().whenever(contextSpy).startActivity(any()) + whenever(handler.post(any())).thenAnswer { (it.arguments[0] as Runnable).run() true @@ -63,56 +81,77 @@ class StylusUsiPowerUiTest : SysuiTestCase() { whenever(btStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) // whenever(btStylusDevice.bluetoothAddress).thenReturn("SO:ME:AD:DR:ES") - stylusUsiPowerUi = StylusUsiPowerUI(mContext, notificationManager, inputManager, handler) + stylusUsiPowerUi = StylusUsiPowerUI(contextSpy, notificationManager, inputManager, handler) + broadcastReceiver = stylusUsiPowerUi.receiver + } + + @Test + fun updateBatteryState_capacityZero_noop() { + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0f)) + + verifyNoMoreInteractions(notificationManager) } @Test fun updateBatteryState_capacityBelowThreshold_notifies() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) - verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) + verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) verifyNoMoreInteractions(notificationManager) } @Test fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f)) - verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) verifyNoMoreInteractions(notificationManager) } @Test fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f)) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) it.verifyNoMoreInteractions() } } @Test fun updateBatteryState_existingNotification_capacityBelowThreshold_updatesNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.15f)) - - verify(notificationManager, times(2)).notify(eq(R.string.stylus_battery_low), any()) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.15f)) + + verify(notificationManager, times(2)) + .notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture()) + assertEquals( + notificationCaptor.value.extras.getString(Notification.EXTRA_TITLE), + context.getString(R.string.stylus_battery_low_percentage, "15%") + ) + assertEquals( + notificationCaptor.value.extras.getString(Notification.EXTRA_TEXT), + context.getString(R.string.stylus_battery_low_subtitle) + ) verifyNoMoreInteractions(notificationManager) } @Test fun updateBatteryState_capacityAboveThenBelowThreshold_hidesThenShowsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.5f)) - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.5f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) it.verifyNoMoreInteractions() } } @@ -121,47 +160,66 @@ class StylusUsiPowerUiTest : SysuiTestCase() { fun updateSuppression_noExistingNotification_cancelsNotification() { stylusUsiPowerUi.updateSuppression(true) - verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) verifyNoMoreInteractions(notificationManager) } @Test fun updateSuppression_existingNotification_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) stylusUsiPowerUi.updateSuppression(true) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) it.verifyNoMoreInteractions() } } @Test @Ignore("TODO(b/257936830): get bt address once input api available") - fun refresh_hasConnectedBluetoothStylus_doesNotNotify() { + fun refresh_hasConnectedBluetoothStylus_cancelsNotification() { whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0)) stylusUsiPowerUi.refresh() - verifyNoMoreInteractions(notificationManager) + verify(notificationManager).cancel(R.string.stylus_battery_low_percentage) } @Test @Ignore("TODO(b/257936830): get bt address once input api available") fun refresh_hasConnectedBluetoothStylus_existingNotification_cancelsNotification() { - stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) + stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f)) whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(0)) stylusUsiPowerUi.refresh() - verify(notificationManager).cancel(R.string.stylus_battery_low) + verify(notificationManager).cancel(R.string.stylus_battery_low_percentage) + } + + @Test + fun broadcastReceiver_clicked_hasInputDeviceId_startsUsiDetailsActivity() { + val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) + val activityIntentCaptor = argumentCaptor<Intent>() + stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.15f)) + broadcastReceiver.onReceive(contextSpy, intent) + + verify(contextSpy, times(1)).startActivity(activityIntentCaptor.capture()) + assertThat(activityIntentCaptor.value.action) + .isEqualTo(StylusUsiPowerUI.ACTION_STYLUS_USI_DETAILS) + val args = + activityIntentCaptor.value.getExtra(StylusUsiPowerUI.KEY_SETTINGS_FRAGMENT_ARGS) + as Bundle + assertThat(args.getInt(StylusUsiPowerUI.KEY_DEVICE_INPUT_ID)).isEqualTo(1) } - class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() { - override fun getCapacity() = capacity - override fun getStatus() = 0 - override fun isPresent() = true + @Test + fun broadcastReceiver_clicked_nullInputDeviceId_doesNotStartActivity() { + val intent = Intent(StylusUsiPowerUI.ACTION_CLICKED_LOW_BATTERY) + broadcastReceiver.onReceive(contextSpy, intent) + + verify(contextSpy, never()).startActivity(any()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 388c51f04e13..dec80807ec87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -24,6 +24,7 @@ import static android.service.notification.NotificationListenerService.REASON_AP import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; import static com.google.common.truth.Truth.assertThat; @@ -228,6 +229,8 @@ public class BubblesTest extends SysuiTestCase { private BubbleEntry mBubbleEntryUser11; private BubbleEntry mBubbleEntry2User11; + private Intent mAppBubbleIntent; + @Mock private ShellInit mShellInit; @Mock @@ -323,6 +326,9 @@ public class BubblesTest extends SysuiTestCase { mBubbleEntry2User11 = BubblesManager.notifToBubbleEntry( mNotificationTestHelper.createBubble(handle)); + mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class); + mAppBubbleIntent.setPackage(mContext.getPackageName()); + mZenModeConfig.suppressedVisualEffects = 0; when(mZenModeController.getConfig()).thenReturn(mZenModeConfig); @@ -1630,6 +1636,62 @@ public class BubblesTest extends SysuiTestCase { any(Bubble.class), anyBoolean(), anyBoolean()); } + @Test + public void testShowOrHideAppBubble_addsAndExpand() { + assertThat(mBubbleController.isStackExpanded()).isFalse(); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull(); + + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + verify(mBubbleController).inflateAndAdd(any(Bubble.class), /* suppressFlyout= */ eq(true), + /* showInShade= */ eq(false)); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + } + + @Test + public void testShowOrHideAppBubble_expandIfCollapsed() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + mBubbleController.updateBubble(mBubbleEntry); + mBubbleController.collapseStack(); + assertThat(mBubbleController.isStackExpanded()).isFalse(); + + // Calling this while collapsed will expand the app bubble + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); + } + + @Test + public void testShowOrHideAppBubble_collapseIfSelected() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + + // Calling this while the app bubble is expanded should collapse the stack + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isFalse(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); + } + + @Test + public void testShowOrHideAppBubble_selectIfNotSelected() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + mBubbleController.updateBubble(mBubbleEntry); + mBubbleController.expandStackAndSelectBubble(mBubbleEntry); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(mBubbleEntry.getKey()); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); + } + /** Creates a bubble using the userId and package. */ private Bubble createBubble(int userId, String pkg) { final UserHandle userHandle = new UserHandle(userId); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java index 3767fbe98dc1..342855357fd2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java @@ -40,24 +40,49 @@ import java.io.IOException; public class MemoryTrackingTestCase extends SysuiTestCase { private static File sFilesDir = null; private static String sLatestTestClassName = null; + private static int sHeapCount = 0; + private static File sLatestBaselineHeapFile = null; - @Before public void grabFilesDir() { + // Ideally, we would do this in @BeforeClass just once, but we need mContext to get the files + // dir, and that does not exist until @Before on each test method. + @Before + public void grabFilesDir() throws IOException { + // This should happen only once per suite if (sFilesDir == null) { sFilesDir = mContext.getFilesDir(); } - sLatestTestClassName = getClass().getName(); + + // This will happen before the first test method in each class + if (sLatestTestClassName == null) { + sLatestTestClassName = getClass().getName(); + sLatestBaselineHeapFile = dump("baseline" + (++sHeapCount), "before-test"); + } } @AfterClass public static void dumpHeap() throws IOException { + File afterTestHeap = dump(sLatestTestClassName, "after-test"); + if (sLatestBaselineHeapFile != null && afterTestHeap != null) { + Log.w("MEMORY", "To compare heap to baseline (use go/ahat):"); + Log.w("MEMORY", " adb pull " + sLatestBaselineHeapFile); + Log.w("MEMORY", " adb pull " + afterTestHeap); + Log.w("MEMORY", + " java -jar ahat.jar --baseline " + sLatestBaselineHeapFile.getName() + " " + + afterTestHeap.getName()); + } + sLatestTestClassName = null; + } + + private static File dump(String basename, String heapKind) throws IOException { if (sFilesDir == null) { Log.e("MEMORY", "Somehow no test cases??"); - return; + return null; } mockitoTearDown(); - Log.w("MEMORY", "about to dump heap"); - File path = new File(sFilesDir, sLatestTestClassName + ".ahprof"); + Log.w("MEMORY", "about to dump " + heapKind + " heap"); + File path = new File(sFilesDir, basename + ".ahprof"); Debug.dumpHprofData(path.getPath()); - Log.w("MEMORY", "did it! Location: " + path); + Log.w("MEMORY", "Success! Location: " + path); + return path; } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.kt new file mode 100644 index 000000000000..f3e52de0d7a0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricRepository.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 com.android.systemui.keyguard.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeBiometricRepository : BiometricRepository { + + private val _isFingerprintEnrolled = MutableStateFlow<Boolean>(false) + override val isFingerprintEnrolled: StateFlow<Boolean> = _isFingerprintEnrolled.asStateFlow() + + private val _isStrongBiometricAllowed = MutableStateFlow(false) + override val isStrongBiometricAllowed = _isStrongBiometricAllowed.asStateFlow() + + private val _isFingerprintEnabledByDevicePolicy = MutableStateFlow(false) + override val isFingerprintEnabledByDevicePolicy = + _isFingerprintEnabledByDevicePolicy.asStateFlow() + + fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) { + _isFingerprintEnrolled.value = isFingerprintEnrolled + } + + fun setStrongBiometricAllowed(isStrongBiometricAllowed: Boolean) { + _isStrongBiometricAllowed.value = isStrongBiometricAllowed + } + + fun setFingerprintEnabledByDevicePolicy(isFingerprintEnabledByDevicePolicy: Boolean) { + _isFingerprintEnabledByDevicePolicy.value = isFingerprintEnabledByDevicePolicy + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 39d2ecaef51a..15b473640de7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -52,6 +52,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _isDozing = MutableStateFlow(false) override val isDozing: Flow<Boolean> = _isDozing + private val _isAodAvailable = MutableStateFlow(false) + override val isAodAvailable: Flow<Boolean> = _isAodAvailable + private val _isDreaming = MutableStateFlow(false) override val isDreaming: Flow<Boolean> = _isDreaming @@ -126,6 +129,10 @@ class FakeKeyguardRepository : KeyguardRepository { _isDozing.value = isDozing } + fun setAodAvailable(isAodAvailable: Boolean) { + _isAodAvailable.value = isAodAvailable + } + fun setDreamingWithOverlay(isDreaming: Boolean) { _isDreamingWithOverlay.value = isDreaming } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 308f36029e02..9d91b9753691 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -817,7 +817,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (host != null) { host.callbacks = null; pruneHostLocked(host); - mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false); + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(), + false); } } } @@ -888,12 +889,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku Host host = lookupHostLocked(id); if (host != null) { - try { - mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false); - } catch (NullPointerException e) { - Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e); - throw e; - } + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(), + false); } } } @@ -4345,14 +4342,15 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku PendingHostUpdate.appWidgetRemoved(appWidgetId)); } - public SparseArray<String> getWidgetUids() { + public SparseArray<String> getWidgetUidsIfBound() { final SparseArray<String> uids = new SparseArray<>(); for (int i = widgets.size() - 1; i >= 0; i--) { final Widget widget = widgets.get(i); if (widget.provider == null) { if (DEBUG) { - Slog.e(TAG, "Widget with no provider " + widget.toString()); + Slog.d(TAG, "Widget with no provider " + widget.toString()); } + continue; } final ProviderId providerId = widget.provider.id; uids.put(providerId.uid, providerId.componentName.getPackageName()); diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 677871f6c85f..8c2c964e2d2c 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -357,6 +357,7 @@ final class SaveUi { params.width = WindowManager.LayoutParams.MATCH_PARENT; params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title); params.windowAnimations = R.style.AutofillSaveAnimation; + params.setTrustedOverlay(); show(); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 9669c060b716..c36e0700c723 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -3420,6 +3420,11 @@ public final class ActiveServices { throw new SecurityException("BIND_EXTERNAL_SERVICE failed, " + className + " is not an isolatedProcess"); } + if (!mAm.getPackageManagerInternal().isSameApp(callingPackage, callingUid, + userId)) { + throw new SecurityException("BIND_EXTERNAL_SERVICE failed, " + + "calling package not owned by calling UID "); + } // Run the service under the calling package's application. ApplicationInfo aInfo = AppGlobals.getPackageManager().getApplicationInfo( callingPackage, ActivityManagerService.STOCK_PM_FLAGS, userId); diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 736914ace215..278c98f45c44 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -82,6 +82,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private final @NonNull AudioService mAudioService; private final @NonNull Context mContext; + private final @NonNull AudioSystemAdapter mAudioSystem; /** ID for Communication strategy retrieved form audio policy manager */ private int mCommunicationStrategyId = -1; @@ -156,12 +157,14 @@ import java.util.concurrent.atomic.AtomicBoolean; public static final long USE_SET_COMMUNICATION_DEVICE = 243827847L; //------------------------------------------------------------------- - /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) { + /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service, + @NonNull AudioSystemAdapter audioSystem) { mContext = context; mAudioService = service; mBtHelper = new BtHelper(this); mDeviceInventory = new AudioDeviceInventory(this); mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext); + mAudioSystem = audioSystem; init(); } @@ -170,12 +173,14 @@ import java.util.concurrent.atomic.AtomicBoolean; * in system_server */ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service, @NonNull AudioDeviceInventory mockDeviceInventory, - @NonNull SystemServerAdapter mockSystemServer) { + @NonNull SystemServerAdapter mockSystemServer, + @NonNull AudioSystemAdapter audioSystem) { mContext = context; mAudioService = service; mBtHelper = new BtHelper(this); mDeviceInventory = mockDeviceInventory; mSystemServer = mockSystemServer; + mAudioSystem = audioSystem; init(); } @@ -450,7 +455,7 @@ import java.util.concurrent.atomic.AtomicBoolean; AudioAttributes attr = AudioProductStrategy.getAudioAttributesForStrategyWithLegacyStreamType( AudioSystem.STREAM_VOICE_CALL); - List<AudioDeviceAttributes> devices = AudioSystem.getDevicesForAttributes( + List<AudioDeviceAttributes> devices = mAudioSystem.getDevicesForAttributes( attr, false /* forVolume */); if (devices.isEmpty()) { if (mAudioService.isPlatformVoice()) { @@ -1225,7 +1230,7 @@ import java.util.concurrent.atomic.AtomicBoolean; Log.v(TAG, "onSetForceUse(useCase<" + useCase + ">, config<" + config + ">, fromA2dp<" + fromA2dp + ">, eventSource<" + eventSource + ">)"); } - AudioSystem.setForceUse(useCase, config); + mAudioSystem.setForceUse(useCase, config); } private void onSendBecomingNoisyIntent() { @@ -1863,9 +1868,9 @@ import java.util.concurrent.atomic.AtomicBoolean; if (preferredCommunicationDevice == null || preferredCommunicationDevice.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) { - AudioSystem.setParameters("BT_SCO=off"); + mAudioSystem.setParameters("BT_SCO=off"); } else { - AudioSystem.setParameters("BT_SCO=on"); + mAudioSystem.setParameters("BT_SCO=on"); } if (preferredCommunicationDevice == null) { AudioDeviceAttributes defaultDevice = getDefaultCommunicationDevice(); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index c804ef2cf8b4..1bd8f1ea1c18 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -183,6 +183,7 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.audio.AudioServiceEvents.DeviceVolumeEvent; import com.android.server.audio.AudioServiceEvents.PhoneStateEvent; +import com.android.server.audio.AudioServiceEvents.VolChangedBroadcastEvent; import com.android.server.audio.AudioServiceEvents.VolumeEvent; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerInternal.UserRestrictionsListener; @@ -1205,7 +1206,7 @@ public class AudioService extends IAudioService.Stub mUseFixedVolume = mContext.getResources().getBoolean( com.android.internal.R.bool.config_useFixedVolume); - mDeviceBroker = new AudioDeviceBroker(mContext, this); + mDeviceBroker = new AudioDeviceBroker(mContext, this, mAudioSystem); mRecordMonitor = new RecordingActivityMonitor(mContext); mRecordMonitor.registerRecordingCallback(mVoiceRecordingActivityMonitor, true); @@ -1637,7 +1638,7 @@ public class AudioService extends IAudioService.Stub synchronized (mSettingsLock) { final int forDock = mDockAudioMediaEnabled ? - AudioSystem.FORCE_ANALOG_DOCK : AudioSystem.FORCE_NONE; + AudioSystem.FORCE_DIGITAL_DOCK : AudioSystem.FORCE_NONE; mDeviceBroker.setForceUse_Async(AudioSystem.FOR_DOCK, forDock, "onAudioServerDied"); sendEncodedSurroundMode(mContentResolver, "onAudioServerDied"); sendEnabledSurroundFormats(mContentResolver, true); @@ -2258,9 +2259,10 @@ public class AudioService extends IAudioService.Stub SENDMSG_QUEUE, AudioSystem.FOR_DOCK, mDockAudioMediaEnabled ? - AudioSystem.FORCE_ANALOG_DOCK : AudioSystem.FORCE_NONE, + AudioSystem.FORCE_DIGITAL_DOCK : AudioSystem.FORCE_NONE, new String("readDockAudioSettings"), 0); + } @@ -3601,9 +3603,11 @@ public class AudioService extends IAudioService.Stub setRingerMode(getNewRingerMode(stream, index, flags), TAG + ".onSetStreamVolume", false /*external*/); } - // setting non-zero volume for a muted stream unmutes the stream and vice versa, + // setting non-zero volume for a muted stream unmutes the stream and vice versa + // (only when changing volume for the current device), // except for BT SCO stream where only explicit mute is allowed to comply to BT requirements - if (streamType != AudioSystem.STREAM_BLUETOOTH_SCO) { + if ((streamType != AudioSystem.STREAM_BLUETOOTH_SCO) + && (getDeviceForStream(stream) == device)) { mStreamStates[stream].mute(index == 0); } } @@ -3741,19 +3745,30 @@ public class AudioService extends IAudioService.Stub Objects.requireNonNull(ada); Objects.requireNonNull(callingPackage); - AudioService.sVolumeLogger.loglogi("setDeviceVolume" + " from:" + callingPackage + " " - + vi + " " + ada, TAG); - if (!vi.hasStreamType()) { Log.e(TAG, "Unsupported non-stream type based VolumeInfo", new Exception()); return; } + int index = vi.getVolumeIndex(); if (index == VolumeInfo.INDEX_NOT_SET && !vi.hasMuteCommand()) { throw new IllegalArgumentException( "changing device volume requires a volume index or mute command"); } + // force a cache clear to force reevaluating stream type to audio device selection + // that can interfere with the sending of the VOLUME_CHANGED_ACTION intent + // TODO change cache management to not rely only on invalidation, but on "do not trust" + // moments when routing is in flux. + mAudioSystem.clearRoutingCache(); + + // log the current device that will be used when evaluating the sending of the + // VOLUME_CHANGED_ACTION intent to see if the current device is the one being modified + final int currDev = getDeviceForStream(vi.getStreamType()); + + AudioService.sVolumeLogger.log(new DeviceVolumeEvent(vi.getStreamType(), index, ada, + currDev, callingPackage)); + // TODO handle unmuting of current audio device // if a stream is not muted but the VolumeInfo is for muting, set the volume index // for the device to min volume @@ -3837,11 +3852,11 @@ public class AudioService extends IAudioService.Stub return; } - final AudioEventLogger.Event event = (device == null) - ? new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType, - index/*val1*/, flags/*val2*/, callingPackage) - : new DeviceVolumeEvent(streamType, index, device, callingPackage); - sVolumeLogger.log(event); + if (device == null) { + // call was already logged in setDeviceVolume() + sVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType, + index/*val1*/, flags/*val2*/, callingPackage)); + } setStreamVolume(streamType, index, flags, device, callingPackage, callingPackage, attributionTag, Binder.getCallingUid(), callingOrSelfHasAudioSettingsPermission()); @@ -4242,7 +4257,11 @@ public class AudioService extends IAudioService.Stub maybeSendSystemAudioStatusCommand(false); } } - sendVolumeUpdate(streamType, oldIndex, index, flags, device); + if (ada == null) { + // only non-null when coming here from setDeviceVolume + // TODO change test to check early if device is current device or not + sendVolumeUpdate(streamType, oldIndex, index, flags, device); + } } @@ -7982,6 +8001,8 @@ public class AudioService extends IAudioService.Stub mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex); mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS, mStreamVolumeAlias[mStreamType]); + AudioService.sVolumeLogger.log(new VolChangedBroadcastEvent( + mStreamType, mStreamVolumeAlias[mStreamType], index)); sendBroadcastToAll(mVolumeChanged); } } @@ -10155,7 +10176,7 @@ public class AudioService extends IAudioService.Stub static final int LOG_NB_EVENTS_PHONE_STATE = 20; static final int LOG_NB_EVENTS_DEVICE_CONNECTION = 50; static final int LOG_NB_EVENTS_FORCE_USE = 20; - static final int LOG_NB_EVENTS_VOLUME = 40; + static final int LOG_NB_EVENTS_VOLUME = 100; static final int LOG_NB_EVENTS_DYN_POLICY = 10; static final int LOG_NB_EVENTS_SPATIAL = 30; diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java index 30a9e0a70e96..c2c3f028abdb 100644 --- a/services/core/java/com/android/server/audio/AudioServiceEvents.java +++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java @@ -147,19 +147,42 @@ public class AudioServiceEvents { } } + static final class VolChangedBroadcastEvent extends AudioEventLogger.Event { + final int mStreamType; + final int mAliasStreamType; + final int mIndex; + + VolChangedBroadcastEvent(int stream, int alias, int index) { + mStreamType = stream; + mAliasStreamType = alias; + mIndex = index; + } + + @Override + public String eventToString() { + return new StringBuilder("sending VOLUME_CHANGED stream:") + .append(AudioSystem.streamToString(mStreamType)) + .append(" index:").append(mIndex) + .append(" alias:").append(AudioSystem.streamToString(mAliasStreamType)) + .toString(); + } + } + static final class DeviceVolumeEvent extends AudioEventLogger.Event { final int mStream; final int mVolIndex; final String mDeviceNativeType; final String mDeviceAddress; final String mCaller; + final int mDeviceForStream; DeviceVolumeEvent(int streamType, int index, @NonNull AudioDeviceAttributes device, - String callingPackage) { + int deviceForStream, String callingPackage) { mStream = streamType; mVolIndex = index; mDeviceNativeType = "0x" + Integer.toHexString(device.getInternalType()); mDeviceAddress = device.getAddress(); + mDeviceForStream = deviceForStream; mCaller = callingPackage; // log metrics new MediaMetrics.Item(MediaMetrics.Name.AUDIO_VOLUME_EVENT) @@ -180,7 +203,9 @@ public class AudioServiceEvents { .append(" index:").append(mVolIndex) .append(" device:").append(mDeviceNativeType) .append(" addr:").append(mDeviceAddress) - .append(") from ").append(mCaller).toString(); + .append(") from ").append(mCaller) + .append(" currDevForStream:Ox").append(Integer.toHexString(mDeviceForStream)) + .toString(); } } diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index c3754eb3e44c..258837116cd6 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -105,6 +105,13 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, } } + public void clearRoutingCache() { + if (DEBUG_CACHE) { + Log.d(TAG, "---- routing cache clear (from java) ----------"); + } + invalidateRoutingCache(); + } + /** * Implementation of AudioSystem.VolumeRangeInitRequestCallback */ @@ -337,6 +344,7 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, * @return */ public int setParameters(String keyValuePairs) { + invalidateRoutingCache(); return AudioSystem.setParameters(keyValuePairs); } diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java index 54be4bbd8f82..186294228d11 100644 --- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -58,13 +58,15 @@ import java.util.function.Consumer; public final class PlaybackActivityMonitor implements AudioPlaybackConfiguration.PlayerDeathMonitor, PlayerFocusEnforcer { - public static final String TAG = "AudioService.PlaybackActivityMonitor"; + public static final String TAG = "AS.PlayActivityMonitor"; /*package*/ static final boolean DEBUG = false; /*package*/ static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1; /*package*/ static final int VOLUME_SHAPER_SYSTEM_FADEOUT_ID = 2; /*package*/ static final int VOLUME_SHAPER_SYSTEM_MUTE_AWAIT_CONNECTION_ID = 3; + /*package*/ static final int VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID = 4; + // ducking settings for a "normal duck" at -14dB private static final VolumeShaper.Configuration DUCK_VSHAPE = new VolumeShaper.Configuration.Builder() .setId(VOLUME_SHAPER_SYSTEM_DUCK_ID) @@ -78,6 +80,22 @@ public final class PlaybackActivityMonitor .build(); private static final VolumeShaper.Configuration DUCK_ID = new VolumeShaper.Configuration(VOLUME_SHAPER_SYSTEM_DUCK_ID); + + // ducking settings for a "strong duck" at -35dB (attenuation factor of 0.017783) + private static final VolumeShaper.Configuration STRONG_DUCK_VSHAPE = + new VolumeShaper.Configuration.Builder() + .setId(VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID) + .setCurve(new float[] { 0.f, 1.f } /* times */, + new float[] { 1.f, 0.017783f } /* volumes */) + .setOptionFlags(VolumeShaper.Configuration.OPTION_FLAG_CLOCK_TIME) + .setDuration(MediaFocusControl.getFocusRampTimeMs( + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, + new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build())) + .build(); + private static final VolumeShaper.Configuration STRONG_DUCK_ID = + new VolumeShaper.Configuration(VOLUME_SHAPER_SYSTEM_STRONG_DUCK_ID); + private static final VolumeShaper.Operation PLAY_CREATE_IF_NEEDED = new VolumeShaper.Operation.Builder(VolumeShaper.Operation.PLAY) .createIfNeeded() @@ -659,11 +677,23 @@ public final class PlaybackActivityMonitor // add the players eligible for ducking to the list, and duck them // (if apcsToDuck is empty, this will at least mark this uid as ducked, so when // players of the same uid start, they will be ducked by DuckingManager.checkDuck()) - mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck); + mDuckingManager.duckUid(loser.getClientUid(), apcsToDuck, reqCausesStrongDuck(winner)); } return true; } + private boolean reqCausesStrongDuck(FocusRequester requester) { + if (requester.getGainRequest() != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) { + return false; + } + final int reqUsage = requester.getAudioAttributes().getUsage(); + if ((reqUsage == AudioAttributes.USAGE_ASSISTANT) + || (reqUsage == AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)) { + return true; + } + return false; + } + @Override public void restoreVShapedPlayers(@NonNull FocusRequester winner) { if (DEBUG) { Log.v(TAG, "unduckPlayers: uids winner=" + winner.getClientUid()); } @@ -939,10 +969,11 @@ public final class PlaybackActivityMonitor private static final class DuckingManager { private final HashMap<Integer, DuckedApp> mDuckers = new HashMap<Integer, DuckedApp>(); - synchronized void duckUid(int uid, ArrayList<AudioPlaybackConfiguration> apcsToDuck) { + synchronized void duckUid(int uid, ArrayList<AudioPlaybackConfiguration> apcsToDuck, + boolean requestCausesStrongDuck) { if (DEBUG) { Log.v(TAG, "DuckingManager: duckUid() uid:"+ uid); } if (!mDuckers.containsKey(uid)) { - mDuckers.put(uid, new DuckedApp(uid)); + mDuckers.put(uid, new DuckedApp(uid, requestCausesStrongDuck)); } final DuckedApp da = mDuckers.get(uid); for (AudioPlaybackConfiguration apc : apcsToDuck) { @@ -989,10 +1020,13 @@ public final class PlaybackActivityMonitor private static final class DuckedApp { private final int mUid; + /** determines whether ducking is done with DUCK_VSHAPE or STRONG_DUCK_VSHAPE */ + private final boolean mUseStrongDuck; private final ArrayList<Integer> mDuckedPlayers = new ArrayList<Integer>(); - DuckedApp(int uid) { + DuckedApp(int uid, boolean useStrongDuck) { mUid = uid; + mUseStrongDuck = useStrongDuck; } void dump(PrintWriter pw) { @@ -1013,9 +1047,9 @@ public final class PlaybackActivityMonitor return; } try { - sEventLogger.log((new DuckEvent(apc, skipRamp)).printLog(TAG)); + sEventLogger.log((new DuckEvent(apc, skipRamp, mUseStrongDuck)).printLog(TAG)); apc.getPlayerProxy().applyVolumeShaper( - DUCK_VSHAPE, + mUseStrongDuck ? STRONG_DUCK_VSHAPE : DUCK_VSHAPE, skipRamp ? PLAY_SKIP_RAMP : PLAY_CREATE_IF_NEEDED); mDuckedPlayers.add(piid); } catch (Exception e) { @@ -1031,7 +1065,7 @@ public final class PlaybackActivityMonitor sEventLogger.log((new AudioEventLogger.StringEvent("unducking piid:" + piid)).printLog(TAG)); apc.getPlayerProxy().applyVolumeShaper( - DUCK_ID, + mUseStrongDuck ? STRONG_DUCK_ID : DUCK_ID, VolumeShaper.Operation.REVERSE); } catch (Exception e) { Log.e(TAG, "Error unducking player piid:" + piid + " uid:" + mUid, e); @@ -1146,13 +1180,17 @@ public final class PlaybackActivityMonitor } static final class DuckEvent extends VolumeShaperEvent { + final boolean mUseStrongDuck; + @Override String getVSAction() { - return "ducking"; + return mUseStrongDuck ? "ducking (strong)" : "ducking"; } - DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp) { + DuckEvent(@NonNull AudioPlaybackConfiguration apc, boolean skipRamp, boolean useStrongDuck) + { super(apc, skipRamp); + mUseStrongDuck = useStrongDuck; } } diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java index 57ea812dbb3a..1924f3c92956 100644 --- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java @@ -96,7 +96,11 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide @Override public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { Slog.d(TAG, "Remove onClientFinished: " + clientMonitor + ", success: " + success); - mCallback.onClientFinished(InternalCleanupClient.this, success); + if (mUnknownHALTemplates.isEmpty()) { + mCallback.onClientFinished(InternalCleanupClient.this, success); + } else { + startCleanupUnknownHalTemplates(); + } } }; diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index 05e83da6a107..787bfb00a554 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -16,15 +16,11 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; -import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START; -import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR; - import android.annotation.NonNull; import android.annotation.Nullable; import android.app.TaskStackListener; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; -import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired; import android.hardware.biometrics.common.ICancellationSignal; @@ -92,7 +88,6 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> private long mSideFpsLastAcquireStartTime; private Runnable mAuthSuccessRunnable; private final Clock mClock; - private boolean mDidFinishSfps; FingerprintAuthenticationClient( @NonNull Context context, @@ -198,9 +193,8 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> @Override protected void handleLifecycleAfterAuth(boolean authenticated) { - if (authenticated && !mDidFinishSfps) { + if (authenticated) { mCallback.onClientFinished(this, true /* success */); - mDidFinishSfps = true; } } @@ -210,13 +204,11 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> return false; } - public void handleAuthenticate( + @Override + public void onAuthenticated( BiometricAuthenticator.Identifier identifier, boolean authenticated, ArrayList<Byte> token) { - if (authenticated && mSensorProps.isAnySidefpsType()) { - Slog.i(TAG, "(sideFPS): No power press detected, sending auth"); - } super.onAuthenticated(identifier, authenticated, token); if (authenticated) { mState = STATE_STOPPED; @@ -227,72 +219,11 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> } @Override - public void onAuthenticated( - BiometricAuthenticator.Identifier identifier, - boolean authenticated, - ArrayList<Byte> token) { - - mHandler.post( - () -> { - long delay = 0; - if (authenticated && mSensorProps.isAnySidefpsType()) { - delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp; - - if (mSideFpsLastAcquireStartTime != -1) { - delay = Math.max(0, - delay - (mClock.millis() - mSideFpsLastAcquireStartTime)); - } - - Slog.i(TAG, "(sideFPS) Auth succeeded, sideFps " - + "waiting for power until: " + delay + "ms"); - } - - if (mHandler.hasMessages(MESSAGE_FINGER_UP)) { - Slog.i(TAG, "Finger up detected, sending auth"); - delay = 0; - } - - mAuthSuccessRunnable = - () -> handleAuthenticate(identifier, authenticated, token); - mHandler.postDelayed( - mAuthSuccessRunnable, - MESSAGE_AUTH_SUCCESS, - delay); - }); - } - - @Override public void onAcquired(@FingerprintAcquired int acquiredInfo, int vendorCode) { // For UDFPS, notify SysUI with acquiredInfo, so that the illumination can be turned off // for most ACQUIRED messages. See BiometricFingerprintConstants#FingerprintAcquired mSensorOverlays.ifUdfps(controller -> controller.onAcquired(getSensorId(), acquiredInfo)); super.onAcquired(acquiredInfo, vendorCode); - if (mSensorProps.isAnySidefpsType()) { - if (acquiredInfo == FINGERPRINT_ACQUIRED_START) { - mSideFpsLastAcquireStartTime = mClock.millis(); - } - final boolean shouldLookForVendor = - mSkipWaitForPowerAcquireMessage == FINGERPRINT_ACQUIRED_VENDOR; - final boolean acquireMessageMatch = acquiredInfo == mSkipWaitForPowerAcquireMessage; - final boolean vendorMessageMatch = vendorCode == mSkipWaitForPowerVendorAcquireMessage; - final boolean ignorePowerPress = - acquireMessageMatch && (!shouldLookForVendor || vendorMessageMatch); - - if (ignorePowerPress) { - Slog.d(TAG, "(sideFPS) onFingerUp"); - mHandler.post(() -> { - if (mHandler.hasMessages(MESSAGE_AUTH_SUCCESS)) { - Slog.d(TAG, "(sideFPS) skipping wait for power"); - mHandler.removeMessages(MESSAGE_AUTH_SUCCESS); - mHandler.post(mAuthSuccessRunnable); - } else { - mHandler.postDelayed(() -> { - }, MESSAGE_FINGER_UP, mFingerUpIgnoresPower); - } - }); - } - } - } @Override @@ -488,22 +419,5 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> } @Override - public void onPowerPressed() { - if (mSensorProps.isAnySidefpsType()) { - Slog.i(TAG, "(sideFPS): onPowerPressed"); - mHandler.post(() -> { - if (mDidFinishSfps) { - return; - } - Slog.i(TAG, "(sideFPS): finishing auth"); - // Ignore auths after a power has been detected - mHandler.removeMessages(MESSAGE_AUTH_SUCCESS); - // Do not call onError() as that will send an additional callback to coex. - mDidFinishSfps = true; - onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true); - stopHalOperation(); - mSensorOverlays.hide(getSensorId()); - }); - } - } + public void onPowerPressed() { } } diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java index 9dd2f8408c56..b9ca57e70b3e 100644 --- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java +++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java @@ -76,6 +76,10 @@ class DeviceStateToLayoutMap { return layout; } + int size() { + return mLayoutMap.size(); + } + private Layout createLayout(int state) { if (mLayoutMap.contains(state)) { Slog.e(TAG, "Attempted to create a second layout for state " + state); diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 927874373df8..2b7fbfb71901 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -402,6 +402,8 @@ public class DisplayDeviceConfig { private static final String STABLE_ID_SUFFIX_FORMAT = "id_%d"; private static final String NO_SUFFIX_FORMAT = "%d"; private static final long STABLE_FLAG = 1L << 62; + private static final int DEFAULT_PEAK_REFRESH_RATE = 0; + private static final int DEFAULT_REFRESH_RATE = 60; private static final int DEFAULT_LOW_REFRESH_RATE = 60; private static final int DEFAULT_HIGH_REFRESH_RATE = 0; private static final int[] DEFAULT_BRIGHTNESS_THRESHOLDS = new int[]{}; @@ -570,17 +572,29 @@ public class DisplayDeviceConfig { * using higher refresh rates, even if display modes with higher refresh rates are available * from hardware composer. Only has an effect if the value is non-zero. */ - private int mDefaultHighRefreshRate = DEFAULT_HIGH_REFRESH_RATE; + private int mDefaultPeakRefreshRate = DEFAULT_PEAK_REFRESH_RATE; /** * The default refresh rate for a given device. This value sets the higher default * refresh rate. If the hardware composer on the device supports display modes with * a higher refresh rate than the default value specified here, the framework may use those * higher refresh rate modes if an app chooses one by setting preferredDisplayModeId or calling - * setFrameRate(). We have historically allowed fallback to mDefaultHighRefreshRate if - * mDefaultLowRefreshRate is set to 0, but this is not supported anymore. + * setFrameRate(). We have historically allowed fallback to mDefaultPeakRefreshRate if + * mDefaultRefreshRate is set to 0, but this is not supported anymore. */ - private int mDefaultLowRefreshRate = DEFAULT_LOW_REFRESH_RATE; + private int mDefaultRefreshRate = DEFAULT_REFRESH_RATE; + + /** + * Default refresh rate in the high zone defined by brightness and ambient thresholds. + * If non-positive, then the refresh rate is unchanged even if thresholds are configured. + */ + private int mDefaultHighBlockingZoneRefreshRate = DEFAULT_HIGH_REFRESH_RATE; + + /** + * Default refresh rate in the zone defined by brightness and ambient thresholds. + * If non-positive, then the refresh rate is unchanged even if thresholds are configured. + */ + private int mDefaultLowBlockingZoneRefreshRate = DEFAULT_LOW_REFRESH_RATE; /** * The display uses different gamma curves for different refresh rates. It's hard for panel @@ -1296,15 +1310,29 @@ public class DisplayDeviceConfig { /** * @return Default peak refresh rate of the associated display */ - public int getDefaultHighRefreshRate() { - return mDefaultHighRefreshRate; + public int getDefaultPeakRefreshRate() { + return mDefaultPeakRefreshRate; } /** * @return Default refresh rate of the associated display */ - public int getDefaultLowRefreshRate() { - return mDefaultLowRefreshRate; + public int getDefaultRefreshRate() { + return mDefaultRefreshRate; + } + + /** + * @return Default refresh rate in the higher blocking zone of the associated display + */ + public int getDefaultHighBlockingZoneRefreshRate() { + return mDefaultHighBlockingZoneRefreshRate; + } + + /** + * @return Default refresh rate in the lower blocking zone of the associated display + */ + public int getDefaultLowBlockingZoneRefreshRate() { + return mDefaultLowBlockingZoneRefreshRate; } /** @@ -1442,8 +1470,10 @@ public class DisplayDeviceConfig { + ", mDdcAutoBrightnessAvailable= " + mDdcAutoBrightnessAvailable + ", mAutoBrightnessAvailable= " + mAutoBrightnessAvailable + "\n" - + ", mDefaultRefreshRate= " + mDefaultLowRefreshRate - + ", mDefaultPeakRefreshRate= " + mDefaultHighRefreshRate + + ", mDefaultLowBlockingZoneRefreshRate= " + mDefaultLowBlockingZoneRefreshRate + + ", mDefaultHighBlockingZoneRefreshRate= " + mDefaultHighBlockingZoneRefreshRate + + ", mDefaultPeakRefreshRate= " + mDefaultPeakRefreshRate + + ", mDefaultRefreshRate= " + mDefaultRefreshRate + ", mLowDisplayBrightnessThresholds= " + Arrays.toString(mLowDisplayBrightnessThresholds) + ", mLowAmbientBrightnessThresholds= " @@ -1757,10 +1787,31 @@ public class DisplayDeviceConfig { BlockingZoneConfig higherBlockingZoneConfig = (refreshRateConfigs == null) ? null : refreshRateConfigs.getHigherBlockingZoneConfigs(); + loadPeakDefaultRefreshRate(refreshRateConfigs); + loadDefaultRefreshRate(refreshRateConfigs); loadLowerRefreshRateBlockingZones(lowerBlockingZoneConfig); loadHigherRefreshRateBlockingZones(higherBlockingZoneConfig); } + private void loadPeakDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) { + if (refreshRateConfigs == null || refreshRateConfigs.getDefaultPeakRefreshRate() == null) { + mDefaultPeakRefreshRate = mContext.getResources().getInteger( + R.integer.config_defaultPeakRefreshRate); + } else { + mDefaultPeakRefreshRate = + refreshRateConfigs.getDefaultPeakRefreshRate().intValue(); + } + } + + private void loadDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) { + if (refreshRateConfigs == null || refreshRateConfigs.getDefaultRefreshRate() == null) { + mDefaultRefreshRate = mContext.getResources().getInteger( + R.integer.config_defaultRefreshRate); + } else { + mDefaultRefreshRate = + refreshRateConfigs.getDefaultRefreshRate().intValue(); + } + } /** * Loads the refresh rate configurations pertaining to the upper blocking zones. @@ -1785,10 +1836,10 @@ public class DisplayDeviceConfig { private void loadHigherBlockingZoneDefaultRefreshRate( BlockingZoneConfig upperBlockingZoneConfig) { if (upperBlockingZoneConfig == null) { - mDefaultHighRefreshRate = mContext.getResources().getInteger( - com.android.internal.R.integer.config_defaultPeakRefreshRate); + mDefaultHighBlockingZoneRefreshRate = mContext.getResources().getInteger( + com.android.internal.R.integer.config_fixedRefreshRateInHighZone); } else { - mDefaultHighRefreshRate = + mDefaultHighBlockingZoneRefreshRate = upperBlockingZoneConfig.getDefaultRefreshRate().intValue(); } } @@ -1800,10 +1851,10 @@ public class DisplayDeviceConfig { private void loadLowerBlockingZoneDefaultRefreshRate( BlockingZoneConfig lowerBlockingZoneConfig) { if (lowerBlockingZoneConfig == null) { - mDefaultLowRefreshRate = mContext.getResources().getInteger( - com.android.internal.R.integer.config_defaultRefreshRate); + mDefaultLowBlockingZoneRefreshRate = mContext.getResources().getInteger( + com.android.internal.R.integer.config_defaultRefreshRateInZone); } else { - mDefaultLowRefreshRate = + mDefaultLowBlockingZoneRefreshRate = lowerBlockingZoneConfig.getDefaultRefreshRate().intValue(); } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 8f35924128bb..5cfe65baeb9d 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -104,6 +104,7 @@ import android.os.UserManager; import android.provider.Settings; import android.sysprop.DisplayProperties; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.EventLog; import android.util.IntArray; @@ -256,6 +257,13 @@ public final class DisplayManagerService extends SystemService { final SparseArray<Pair<IVirtualDevice, DisplayWindowPolicyController>> mDisplayWindowPolicyControllers = new SparseArray<>(); + /** + * Map of every internal primary display device {@link HighBrightnessModeMetadata}s indexed by + * {@link DisplayDevice#mUniqueId}. + */ + public final ArrayMap<String, HighBrightnessModeMetadata> mHighBrightnessModeMetadataMap = + new ArrayMap<>(); + // List of all currently registered display adapters. private final ArrayList<DisplayAdapter> mDisplayAdapters = new ArrayList<DisplayAdapter>(); @@ -1570,7 +1578,16 @@ public final class DisplayManagerService extends SystemService { DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { - dpc.onDisplayChanged(); + final DisplayDevice device = display.getPrimaryDisplayDeviceLocked(); + if (device == null) { + Slog.wtf(TAG, "Display Device is null in DisplayManagerService for display: " + + display.getDisplayIdLocked()); + return; + } + + final String uniqueId = device.getUniqueId(); + HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMap.get(uniqueId); + dpc.onDisplayChanged(hbmMetadata); } } @@ -1627,7 +1644,15 @@ public final class DisplayManagerService extends SystemService { final int displayId = display.getDisplayIdLocked(); final DisplayPowerController dpc = mDisplayPowerControllers.get(displayId); if (dpc != null) { - dpc.onDisplayChanged(); + final DisplayDevice device = display.getPrimaryDisplayDeviceLocked(); + if (device == null) { + Slog.wtf(TAG, "Display Device is null in DisplayManagerService for display: " + + display.getDisplayIdLocked()); + return; + } + final String uniqueId = device.getUniqueId(); + HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMap.get(uniqueId); + dpc.onDisplayChanged(hbmMetadata); } } @@ -2611,6 +2636,31 @@ public final class DisplayManagerService extends SystemService { mLogicalDisplayMapper.forEachLocked(this::addDisplayPowerControllerLocked); } + private HighBrightnessModeMetadata getHighBrightnessModeMetadata(LogicalDisplay display) { + final DisplayDevice device = display.getPrimaryDisplayDeviceLocked(); + if (device == null) { + Slog.wtf(TAG, "Display Device is null in DisplayPowerController for display: " + + display.getDisplayIdLocked()); + return null; + } + + // HBM brightness mode is only applicable to internal physical displays. + if (display.getDisplayInfoLocked().type != Display.TYPE_INTERNAL) { + return null; + } + + final String uniqueId = device.getUniqueId(); + + if (mHighBrightnessModeMetadataMap.containsKey(uniqueId)) { + return mHighBrightnessModeMetadataMap.get(uniqueId); + } + + // HBM Time info not present. Create a new one for this physical display. + HighBrightnessModeMetadata hbmInfo = new HighBrightnessModeMetadata(); + mHighBrightnessModeMetadataMap.put(uniqueId, hbmInfo); + return hbmInfo; + } + private void addDisplayPowerControllerLocked(LogicalDisplay display) { if (mPowerHandler == null) { // initPowerManagement has not yet been called. @@ -2622,10 +2672,18 @@ public final class DisplayManagerService extends SystemService { final BrightnessSetting brightnessSetting = new BrightnessSetting(mPersistentDataStore, display, mSyncRoot); + + // If display is internal and has a HighBrightnessModeMetadata mapping, use that. + // Or create a new one and use that. + // We also need to pass a mapping of the HighBrightnessModeTimeInfoMap to + // displayPowerController, so the hbm info can be correctly associated + // with the corresponding displaydevice. + HighBrightnessModeMetadata hbmMetadata = getHighBrightnessModeMetadata(display); + final DisplayPowerController displayPowerController = new DisplayPowerController( mContext, mDisplayPowerCallbacks, mPowerHandler, mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting, - () -> handleBrightnessChange(display)); + () -> handleBrightnessChange(display), hbmMetadata); mDisplayPowerControllers.append(display.getDisplayIdLocked(), displayPowerController); } diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java index aafba5a2a1b4..fdfc20afee79 100644 --- a/services/core/java/com/android/server/display/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/DisplayModeDirector.java @@ -1169,7 +1169,7 @@ public class DisplayModeDirector { mDefaultRefreshRate = (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger( R.integer.config_defaultRefreshRate) - : (float) displayDeviceConfig.getDefaultLowRefreshRate(); + : (float) displayDeviceConfig.getDefaultRefreshRate(); } public void observe() { @@ -1256,7 +1256,7 @@ public class DisplayModeDirector { defaultPeakRefreshRate = (displayDeviceConfig == null) ? (float) mContext.getResources().getInteger( R.integer.config_defaultPeakRefreshRate) - : (float) displayDeviceConfig.getDefaultHighRefreshRate(); + : (float) displayDeviceConfig.getDefaultPeakRefreshRate(); } mDefaultPeakRefreshRate = defaultPeakRefreshRate; } @@ -1612,8 +1612,26 @@ public class DisplayModeDirector { return mHighAmbientBrightnessThresholds; } + /** + * @return the refresh rate to lock to when in a high brightness zone + */ + @VisibleForTesting + int getRefreshRateInHighZone() { + return mRefreshRateInHighZone; + } + + /** + * @return the refresh rate to lock to when in a low brightness zone + */ + @VisibleForTesting + int getRefreshRateInLowZone() { + return mRefreshRateInLowZone; + } + private void loadLowBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig, boolean attemptLoadingFromDeviceConfig) { + loadRefreshRateInHighZone(displayDeviceConfig, attemptLoadingFromDeviceConfig); + loadRefreshRateInLowZone(displayDeviceConfig, attemptLoadingFromDeviceConfig); mLowDisplayBrightnessThresholds = loadBrightnessThresholds( () -> mDeviceConfigDisplaySettings.getLowDisplayBrightnessThresholds(), () -> displayDeviceConfig.getLowDisplayBrightnessThresholds(), @@ -1634,6 +1652,44 @@ public class DisplayModeDirector { } } + private void loadRefreshRateInLowZone(DisplayDeviceConfig displayDeviceConfig, + boolean attemptLoadingFromDeviceConfig) { + int refreshRateInLowZone = + (displayDeviceConfig == null) ? mContext.getResources().getInteger( + R.integer.config_defaultRefreshRateInZone) + : displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate(); + if (attemptLoadingFromDeviceConfig) { + try { + refreshRateInLowZone = mDeviceConfig.getInt( + DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, + refreshRateInLowZone); + } catch (Exception exception) { + // Do nothing + } + } + mRefreshRateInLowZone = refreshRateInLowZone; + } + + private void loadRefreshRateInHighZone(DisplayDeviceConfig displayDeviceConfig, + boolean attemptLoadingFromDeviceConfig) { + int refreshRateInHighZone = + (displayDeviceConfig == null) ? mContext.getResources().getInteger( + R.integer.config_fixedRefreshRateInHighZone) : displayDeviceConfig + .getDefaultHighBlockingZoneRefreshRate(); + if (attemptLoadingFromDeviceConfig) { + try { + refreshRateInHighZone = mDeviceConfig.getInt( + DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE, + refreshRateInHighZone); + } catch (Exception exception) { + // Do nothing + } + } + mRefreshRateInHighZone = refreshRateInHighZone; + } + private void loadHighBrightnessThresholds(DisplayDeviceConfig displayDeviceConfig, boolean attemptLoadingFromDeviceConfig) { mHighDisplayBrightnessThresholds = loadBrightnessThresholds( @@ -1687,14 +1743,6 @@ public class DisplayModeDirector { } /** - * @return the refresh to lock to when in a low brightness zone - */ - @VisibleForTesting - int getRefreshRateInLowZone() { - return mRefreshRateInLowZone; - } - - /** * @return the display brightness thresholds for the low brightness zones */ @VisibleForTesting @@ -1739,8 +1787,17 @@ public class DisplayModeDirector { mHighAmbientBrightnessThresholds = highAmbientBrightnessThresholds; } - mRefreshRateInLowZone = mDeviceConfigDisplaySettings.getRefreshRateInLowZone(); - mRefreshRateInHighZone = mDeviceConfigDisplaySettings.getRefreshRateInHighZone(); + final int refreshRateInLowZone = mDeviceConfigDisplaySettings + .getRefreshRateInLowZone(); + if (refreshRateInLowZone != -1) { + mRefreshRateInLowZone = refreshRateInLowZone; + } + + final int refreshRateInHighZone = mDeviceConfigDisplaySettings + .getRefreshRateInHighZone(); + if (refreshRateInHighZone != -1) { + mRefreshRateInHighZone = refreshRateInHighZone; + } restartObserver(); mDeviceConfigDisplaySettings.startListening(); @@ -1794,6 +1851,10 @@ public class DisplayModeDirector { restartObserver(); } + /** + * Used to reload the lower blocking zone refresh rate in case of changes in the + * DeviceConfig properties. + */ public void onDeviceConfigRefreshRateInLowZoneChanged(int refreshRate) { if (refreshRate != mRefreshRateInLowZone) { mRefreshRateInLowZone = refreshRate; @@ -1817,6 +1878,10 @@ public class DisplayModeDirector { restartObserver(); } + /** + * Used to reload the higher blocking zone refresh rate in case of changes in the + * DeviceConfig properties. + */ public void onDeviceConfigRefreshRateInHighZoneChanged(int refreshRate) { if (refreshRate != mRefreshRateInHighZone) { mRefreshRateInHighZone = refreshRate; @@ -2664,15 +2729,10 @@ public class DisplayModeDirector { } public int getRefreshRateInLowZone() { - int defaultRefreshRateInZone = mContext.getResources().getInteger( - R.integer.config_defaultRefreshRateInZone); - - int refreshRate = mDeviceConfig.getInt( + return mDeviceConfig.getInt( DeviceConfig.NAMESPACE_DISPLAY_MANAGER, - DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, - defaultRefreshRateInZone); + DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE, -1); - return refreshRate; } /* @@ -2694,15 +2754,10 @@ public class DisplayModeDirector { } public int getRefreshRateInHighZone() { - int defaultRefreshRateInZone = mContext.getResources().getInteger( - R.integer.config_fixedRefreshRateInHighZone); - - int refreshRate = mDeviceConfig.getInt( + return mDeviceConfig.getInt( DeviceConfig.NAMESPACE_DISPLAY_MANAGER, DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE, - defaultRefreshRateInZone); - - return refreshRate; + -1); } public int getRefreshRateInHbmSunlight() { @@ -2750,23 +2805,29 @@ public class DisplayModeDirector { int[] lowDisplayBrightnessThresholds = getLowDisplayBrightnessThresholds(); int[] lowAmbientBrightnessThresholds = getLowAmbientBrightnessThresholds(); - int refreshRateInLowZone = getRefreshRateInLowZone(); + final int refreshRateInLowZone = getRefreshRateInLowZone(); mHandler.obtainMessage(MSG_LOW_BRIGHTNESS_THRESHOLDS_CHANGED, new Pair<>(lowDisplayBrightnessThresholds, lowAmbientBrightnessThresholds)) .sendToTarget(); - mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone, 0) + + if (refreshRateInLowZone != -1) { + mHandler.obtainMessage(MSG_REFRESH_RATE_IN_LOW_ZONE_CHANGED, refreshRateInLowZone) .sendToTarget(); + } int[] highDisplayBrightnessThresholds = getHighDisplayBrightnessThresholds(); int[] highAmbientBrightnessThresholds = getHighAmbientBrightnessThresholds(); - int refreshRateInHighZone = getRefreshRateInHighZone(); + final int refreshRateInHighZone = getRefreshRateInHighZone(); mHandler.obtainMessage(MSG_HIGH_BRIGHTNESS_THRESHOLDS_CHANGED, new Pair<>(highDisplayBrightnessThresholds, highAmbientBrightnessThresholds)) .sendToTarget(); - mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone, 0) + + if (refreshRateInHighZone != -1) { + mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HIGH_ZONE_CHANGED, refreshRateInHighZone) .sendToTarget(); + } final int refreshRateInHbmSunlight = getRefreshRateInHbmSunlight(); mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED, diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index f88a3372a4ac..b431306e294d 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -391,6 +391,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private float[] mNitsRange; private final HighBrightnessModeController mHbmController; + private final HighBrightnessModeMetadata mHighBrightnessModeMetadata; private final BrightnessThrottler mBrightnessThrottler; @@ -511,7 +512,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call DisplayPowerCallbacks callbacks, Handler handler, SensorManager sensorManager, DisplayBlanker blanker, LogicalDisplay logicalDisplay, BrightnessTracker brightnessTracker, BrightnessSetting brightnessSetting, - Runnable onBrightnessChangeRunnable) { + Runnable onBrightnessChangeRunnable, HighBrightnessModeMetadata hbmMetadata) { mLogicalDisplay = logicalDisplay; mDisplayId = mLogicalDisplay.getDisplayIdLocked(); final String displayIdStr = "[" + mDisplayId + "]"; @@ -521,6 +522,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mSuspendBlockerIdProxPositive = displayIdStr + "prox positive"; mSuspendBlockerIdProxNegative = displayIdStr + "prox negative"; mSuspendBlockerIdProxDebounce = displayIdStr + "prox debounce"; + mHighBrightnessModeMetadata = hbmMetadata; mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); mUniqueDisplayId = logicalDisplay.getPrimaryDisplayDeviceLocked().getUniqueId(); @@ -793,7 +795,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call * of each display need to be properly reflected in AutomaticBrightnessController. */ @GuardedBy("DisplayManagerService.mSyncRoot") - public void onDisplayChanged() { + public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata) { final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); if (device == null) { Slog.wtf(TAG, "Display Device is null in DisplayPowerController for display: " @@ -815,7 +817,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mUniqueDisplayId = uniqueId; mDisplayStatsId = mUniqueDisplayId.hashCode(); mDisplayDeviceConfig = config; - loadFromDisplayDeviceConfig(token, info); + loadFromDisplayDeviceConfig(token, info, hbmMetadata); /// Since the underlying display-device changed, we really don't know the // last command that was sent to change it's state. Lets assume it is unknown so @@ -872,7 +874,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call } } - private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info) { + private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info, + HighBrightnessModeMetadata hbmMetadata) { // All properties that depend on the associated DisplayDevice and the DDC must be // updated here. loadBrightnessRampRates(); @@ -885,6 +888,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessRampIncreaseMaxTimeMillis, mBrightnessRampDecreaseMaxTimeMillis); } + mHbmController.setHighBrightnessModeMetadata(hbmMetadata); mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId, mDisplayDeviceConfig.getHighBrightnessModeData(), new HighBrightnessModeController.HdrBrightnessDeviceConfig() { @@ -1965,7 +1969,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call if (mAutomaticBrightnessController != null) { mAutomaticBrightnessController.update(); } - }, mContext); + }, mHighBrightnessModeMetadata, mContext); } private BrightnessThrottler createBrightnessThrottlerLocked() { diff --git a/services/core/java/com/android/server/display/HbmEvent.java b/services/core/java/com/android/server/display/HbmEvent.java new file mode 100644 index 000000000000..5675e2f69230 --- /dev/null +++ b/services/core/java/com/android/server/display/HbmEvent.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + + +/** + * Represents an event in which High Brightness Mode was enabled. + */ +class HbmEvent { + private long mStartTimeMillis; + private long mEndTimeMillis; + + HbmEvent(long startTimeMillis, long endTimeMillis) { + this.mStartTimeMillis = startTimeMillis; + this.mEndTimeMillis = endTimeMillis; + } + + public long getStartTimeMillis() { + return mStartTimeMillis; + } + + public long getEndTimeMillis() { + return mEndTimeMillis; + } + + @Override + public String toString() { + return "HbmEvent: {startTimeMillis:" + mStartTimeMillis + ", endTimeMillis: " + + mEndTimeMillis + "}, total: " + + ((mEndTimeMillis - mStartTimeMillis) / 1000) + "]"; + } +} diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java index 0b9d4debd16f..ac32d53daeab 100644 --- a/services/core/java/com/android/server/display/HighBrightnessModeController.java +++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java @@ -42,8 +42,8 @@ import com.android.server.display.DisplayDeviceConfig.HighBrightnessModeData; import com.android.server.display.DisplayManagerService.Clock; import java.io.PrintWriter; +import java.util.ArrayDeque; import java.util.Iterator; -import java.util.LinkedList; /** * Controls the status of high-brightness mode for devices that support it. This class assumes that @@ -105,30 +105,24 @@ class HighBrightnessModeController { private int mHbmStatsState = FrameworkStatsLog.DISPLAY_HBM_STATE_CHANGED__STATE__HBM_OFF; /** - * If HBM is currently running, this is the start time for the current HBM session. + * If HBM is currently running, this is the start time and set of all events, + * for the current HBM session. */ - private long mRunningStartTimeMillis = -1; - - /** - * List of previous HBM-events ordered from most recent to least recent. - * Meant to store only the events that fall into the most recent - * {@link mHbmData.timeWindowMillis}. - */ - private LinkedList<HbmEvent> mEvents = new LinkedList<>(); + private HighBrightnessModeMetadata mHighBrightnessModeMetadata = null; HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken, String displayUniqueId, float brightnessMin, float brightnessMax, HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg, - Runnable hbmChangeCallback, Context context) { + Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata, Context context) { this(new Injector(), handler, width, height, displayToken, displayUniqueId, brightnessMin, - brightnessMax, hbmData, hdrBrightnessCfg, hbmChangeCallback, context); + brightnessMax, hbmData, hdrBrightnessCfg, hbmChangeCallback, hbmMetadata, context); } @VisibleForTesting HighBrightnessModeController(Injector injector, Handler handler, int width, int height, IBinder displayToken, String displayUniqueId, float brightnessMin, float brightnessMax, HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg, - Runnable hbmChangeCallback, Context context) { + Runnable hbmChangeCallback, HighBrightnessModeMetadata hbmMetadata, Context context) { mInjector = injector; mContext = context; mClock = injector.getClock(); @@ -137,6 +131,7 @@ class HighBrightnessModeController { mBrightnessMin = brightnessMin; mBrightnessMax = brightnessMax; mHbmChangeCallback = hbmChangeCallback; + mHighBrightnessModeMetadata = hbmMetadata; mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler); mSettingsObserver = new SettingsObserver(mHandler); mRecalcRunnable = this::recalculateTimeAllowance; @@ -222,19 +217,22 @@ class HighBrightnessModeController { // If we are starting or ending a high brightness mode session, store the current // session in mRunningStartTimeMillis, or the old one in mEvents. - final boolean wasHbmDrainingAvailableTime = mRunningStartTimeMillis != -1; + final long runningStartTime = mHighBrightnessModeMetadata.getRunningStartTimeMillis(); + final boolean wasHbmDrainingAvailableTime = runningStartTime != -1; final boolean shouldHbmDrainAvailableTime = mBrightness > mHbmData.transitionPoint && !mIsHdrLayerPresent; if (wasHbmDrainingAvailableTime != shouldHbmDrainAvailableTime) { final long currentTime = mClock.uptimeMillis(); if (shouldHbmDrainAvailableTime) { - mRunningStartTimeMillis = currentTime; + mHighBrightnessModeMetadata.setRunningStartTimeMillis(currentTime); } else { - mEvents.addFirst(new HbmEvent(mRunningStartTimeMillis, currentTime)); - mRunningStartTimeMillis = -1; + final HbmEvent hbmEvent = new HbmEvent(runningStartTime, currentTime); + mHighBrightnessModeMetadata.addHbmEvent(hbmEvent); + mHighBrightnessModeMetadata.setRunningStartTimeMillis(-1); if (DEBUG) { - Slog.d(TAG, "New HBM event: " + mEvents.getFirst()); + Slog.d(TAG, "New HBM event: " + + mHighBrightnessModeMetadata.getHbmEventQueue().peekFirst()); } } } @@ -260,6 +258,10 @@ class HighBrightnessModeController { mSettingsObserver.stopObserving(); } + void setHighBrightnessModeMetadata(HighBrightnessModeMetadata hbmInfo) { + mHighBrightnessModeMetadata = hbmInfo; + } + void resetHbmData(int width, int height, IBinder displayToken, String displayUniqueId, HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg) { mWidth = width; @@ -316,20 +318,22 @@ class HighBrightnessModeController { pw.println(" mBrightnessMax=" + mBrightnessMax); pw.println(" remainingTime=" + calculateRemainingTime(mClock.uptimeMillis())); pw.println(" mIsTimeAvailable= " + mIsTimeAvailable); - pw.println(" mRunningStartTimeMillis=" + TimeUtils.formatUptime(mRunningStartTimeMillis)); + pw.println(" mRunningStartTimeMillis=" + + TimeUtils.formatUptime(mHighBrightnessModeMetadata.getRunningStartTimeMillis())); pw.println(" mIsThermalStatusWithinLimit=" + mIsThermalStatusWithinLimit); pw.println(" mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode); pw.println(" width*height=" + mWidth + "*" + mHeight); pw.println(" mEvents="); final long currentTime = mClock.uptimeMillis(); long lastStartTime = currentTime; - if (mRunningStartTimeMillis != -1) { - lastStartTime = dumpHbmEvent(pw, new HbmEvent(mRunningStartTimeMillis, currentTime)); + long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis(); + if (runningStartTimeMillis != -1) { + lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime)); } - for (HbmEvent event : mEvents) { - if (lastStartTime > event.endTimeMillis) { + for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) { + if (lastStartTime > event.getEndTimeMillis()) { pw.println(" event: [normal brightness]: " - + TimeUtils.formatDuration(lastStartTime - event.endTimeMillis)); + + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis())); } lastStartTime = dumpHbmEvent(pw, event); } @@ -338,12 +342,12 @@ class HighBrightnessModeController { } private long dumpHbmEvent(PrintWriter pw, HbmEvent event) { - final long duration = event.endTimeMillis - event.startTimeMillis; + final long duration = event.getEndTimeMillis() - event.getStartTimeMillis(); pw.println(" event: [" - + TimeUtils.formatUptime(event.startTimeMillis) + ", " - + TimeUtils.formatUptime(event.endTimeMillis) + "] (" + + TimeUtils.formatUptime(event.getStartTimeMillis()) + ", " + + TimeUtils.formatUptime(event.getEndTimeMillis()) + "] (" + TimeUtils.formatDuration(duration) + ")"); - return event.startTimeMillis; + return event.getStartTimeMillis(); } private boolean isCurrentlyAllowed() { @@ -372,13 +376,15 @@ class HighBrightnessModeController { // First, lets see how much time we've taken for any currently running // session of HBM. - if (mRunningStartTimeMillis > 0) { - if (mRunningStartTimeMillis > currentTime) { + long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis(); + if (runningStartTimeMillis > 0) { + if (runningStartTimeMillis > currentTime) { Slog.e(TAG, "Start time set to the future. curr: " + currentTime - + ", start: " + mRunningStartTimeMillis); - mRunningStartTimeMillis = currentTime; + + ", start: " + runningStartTimeMillis); + mHighBrightnessModeMetadata.setRunningStartTimeMillis(currentTime); + runningStartTimeMillis = currentTime; } - timeAlreadyUsed = currentTime - mRunningStartTimeMillis; + timeAlreadyUsed = currentTime - runningStartTimeMillis; } if (DEBUG) { @@ -387,18 +393,19 @@ class HighBrightnessModeController { // Next, lets iterate through the history of previous sessions and add those times. final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis; - Iterator<HbmEvent> it = mEvents.iterator(); + Iterator<HbmEvent> it = mHighBrightnessModeMetadata.getHbmEventQueue().iterator(); while (it.hasNext()) { final HbmEvent event = it.next(); // If this event ended before the current Timing window, discard forever and ever. - if (event.endTimeMillis < windowstartTimeMillis) { + if (event.getEndTimeMillis() < windowstartTimeMillis) { it.remove(); continue; } - final long startTimeMillis = Math.max(event.startTimeMillis, windowstartTimeMillis); - timeAlreadyUsed += event.endTimeMillis - startTimeMillis; + final long startTimeMillis = Math.max(event.getStartTimeMillis(), + windowstartTimeMillis); + timeAlreadyUsed += event.getEndTimeMillis() - startTimeMillis; } if (DEBUG) { @@ -425,17 +432,18 @@ class HighBrightnessModeController { // Calculate the time at which we want to recalculate mIsTimeAvailable in case a lux or // brightness change doesn't happen before then. long nextTimeout = -1; + final ArrayDeque<HbmEvent> hbmEvents = mHighBrightnessModeMetadata.getHbmEventQueue(); if (mBrightness > mHbmData.transitionPoint) { // if we're in high-lux now, timeout when we run out of allowed time. nextTimeout = currentTime + remainingTime; - } else if (!mIsTimeAvailable && mEvents.size() > 0) { + } else if (!mIsTimeAvailable && hbmEvents.size() > 0) { // If we are not allowed...timeout when the oldest event moved outside of the timing // window by at least minTime. Basically, we're calculating the soonest time we can // get {@code timeMinMillis} back to us. final long windowstartTimeMillis = currentTime - mHbmData.timeWindowMillis; - final HbmEvent lastEvent = mEvents.getLast(); + final HbmEvent lastEvent = hbmEvents.peekLast(); final long startTimePlusMinMillis = - Math.max(windowstartTimeMillis, lastEvent.startTimeMillis) + Math.max(windowstartTimeMillis, lastEvent.getStartTimeMillis()) + mHbmData.timeMinMillis; final long timeWhenMinIsGainedBack = currentTime + (startTimePlusMinMillis - windowstartTimeMillis) - remainingTime; @@ -459,9 +467,10 @@ class HighBrightnessModeController { + ", mUnthrottledBrightness: " + mUnthrottledBrightness + ", mThrottlingReason: " + BrightnessInfo.briMaxReasonToString(mThrottlingReason) - + ", RunningStartTimeMillis: " + mRunningStartTimeMillis + + ", RunningStartTimeMillis: " + + mHighBrightnessModeMetadata.getRunningStartTimeMillis() + ", nextTimeout: " + (nextTimeout != -1 ? (nextTimeout - currentTime) : -1) - + ", events: " + mEvents); + + ", events: " + hbmEvents); } if (nextTimeout != -1) { @@ -588,25 +597,6 @@ class HighBrightnessModeController { } } - /** - * Represents an event in which High Brightness Mode was enabled. - */ - private static class HbmEvent { - public long startTimeMillis; - public long endTimeMillis; - - HbmEvent(long startTimeMillis, long endTimeMillis) { - this.startTimeMillis = startTimeMillis; - this.endTimeMillis = endTimeMillis; - } - - @Override - public String toString() { - return "[Event: {" + startTimeMillis + ", " + endTimeMillis + "}, total: " - + ((endTimeMillis - startTimeMillis) / 1000) + "]"; - } - } - @VisibleForTesting class HdrListener extends SurfaceControlHdrLayerInfoListener { @Override diff --git a/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java b/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java new file mode 100644 index 000000000000..37234ff0bf19 --- /dev/null +++ b/services/core/java/com/android/server/display/HighBrightnessModeMetadata.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + +import java.util.ArrayDeque; + + +/** + * Represents High Brightness Mode metadata associated + * with a specific internal physical display. + * Required for separately storing data like time information, + * and related events when display was in HBM mode per + * physical internal display. + */ +class HighBrightnessModeMetadata { + /** + * Queue of previous HBM-events ordered from most recent to least recent. + * Meant to store only the events that fall into the most recent + * {@link HighBrightnessModeData#timeWindowMillis mHbmData.timeWindowMillis}. + */ + private final ArrayDeque<HbmEvent> mEvents = new ArrayDeque<>(); + + /** + * If HBM is currently running, this is the start time for the current HBM session. + */ + private long mRunningStartTimeMillis = -1; + + public long getRunningStartTimeMillis() { + return mRunningStartTimeMillis; + } + + public void setRunningStartTimeMillis(long setTime) { + mRunningStartTimeMillis = setTime; + } + + public ArrayDeque<HbmEvent> getHbmEventQueue() { + return mEvents; + } + + public void addHbmEvent(HbmEvent hbmEvent) { + mEvents.addFirst(hbmEvent); + } +} + diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index bf576b8909f2..375e51c672b5 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -372,12 +372,23 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { void setDeviceStateLocked(int state, boolean isOverrideActive) { Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState + ", interactive=" + mInteractive + ", mBootCompleted=" + mBootCompleted); + mPendingDeviceState = state; + + if (!mBootCompleted) { + // The boot animation might still be in progress, we do not want to switch states now + // as the boot animation would end up with an incorrect size. + if (DEBUG) { + Slog.d(TAG, "Postponing transition to state: " + mPendingDeviceState + + " until boot is completed"); + } + return; + } + // As part of a state transition, we may need to turn off some displays temporarily so that // the transition is smooth. Plus, on some devices, only one internal displays can be // on at a time. We use LogicalDisplay.setIsInTransition to mark a display that needs to be // temporarily turned off. resetLayoutLocked(mDeviceState, state, /* isStateChangeStarting= */ true); - mPendingDeviceState = state; final boolean wakeDevice = shouldDeviceBeWoken(mPendingDeviceState, mDeviceState, mInteractive, mBootCompleted); final boolean sleepDevice = shouldDeviceBePutToSleep(mPendingDeviceState, mDeviceState, @@ -424,6 +435,9 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { void onBootCompleted() { synchronized (mSyncRoot) { mBootCompleted = true; + if (mPendingDeviceState != DeviceStateManager.INVALID_DEVICE_STATE) { + setDeviceStateLocked(mPendingDeviceState, /* isOverrideActive= */ false); + } } } @@ -926,6 +940,15 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { final int layerStack = assignLayerStackLocked(displayId); final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device); display.updateLocked(mDisplayDeviceRepo); + + final DisplayInfo info = display.getDisplayInfoLocked(); + if (info.type == Display.TYPE_INTERNAL && mDeviceStateToLayoutMap.size() > 1) { + // If this is an internal display and the device uses a display layout configuration, + // the display should be disabled as later we will receive a device state update, which + // will tell us which internal displays should be enabled and which should be disabled. + display.setEnabledLocked(false); + } + mLogicalDisplays.put(displayId, display); return display; } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 554e2690b878..20c9a211e586 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -4215,7 +4215,6 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } boolean changed = false; - Set<Permission> needsUpdate = null; synchronized (mLock) { final Iterator<Permission> it = mRegistry.getPermissionTrees().iterator(); while (it.hasNext()) { @@ -4234,26 +4233,6 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt + " that used to be declared by " + bp.getPackageName()); it.remove(); } - if (needsUpdate == null) { - needsUpdate = new ArraySet<>(); - } - needsUpdate.add(bp); - } - } - if (needsUpdate != null) { - for (final Permission bp : needsUpdate) { - final AndroidPackage sourcePkg = - mPackageManagerInt.getPackage(bp.getPackageName()); - final PackageStateInternal sourcePs = - mPackageManagerInt.getPackageStateInternal(bp.getPackageName()); - synchronized (mLock) { - if (sourcePkg != null && sourcePs != null) { - continue; - } - Slog.w(TAG, "Removing dangling permission tree: " + bp.getName() - + " from package " + bp.getPackageName()); - mRegistry.removePermission(bp.getName()); - } } } return changed; diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 5285f63dcc44..b55b6dd0eb55 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4129,9 +4129,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_DEMO_APP_2: case KeyEvent.KEYCODE_DEMO_APP_3: case KeyEvent.KEYCODE_DEMO_APP_4: { - // TODO(b/254604589): Dispatch KeyEvent to System UI. - sendSystemKeyToStatusBarAsync(keyCode); - // Just drop if keys are not intercepted for direct key. result &= ~ACTION_PASS_TO_USER; break; diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index abaa3630ff7b..0ea6157dd2a4 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -892,7 +892,7 @@ public class AppTransitionController { * * TODO(b/213312721): Remove this predicate and its callers once ShellTransition is enabled. */ - private static boolean isTaskViewTask(WindowContainer wc) { + static boolean isTaskViewTask(WindowContainer wc) { // We use Task#mRemoveWithTaskOrganizer to identify an embedded Task, but this is a hack and // it is not guaranteed to work this logic in the future version. return wc instanceof Task && ((Task) wc).mRemoveWithTaskOrganizer; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index a259baae1277..a464112f0492 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -6411,6 +6411,11 @@ class Task extends TaskFragment { return this; } + Builder setRemoveWithTaskOrganizer(boolean removeWithTaskOrganizer) { + mRemoveWithTaskOrganizer = removeWithTaskOrganizer; + return this; + } + private Builder setUserId(int userId) { mUserId = userId; return this; @@ -6608,7 +6613,7 @@ class Task extends TaskFragment { mCallingPackage = mActivityInfo.packageName; mResizeMode = mActivityInfo.resizeMode; mSupportsPictureInPicture = mActivityInfo.supportsPictureInPicture(); - if (mActivityOptions != null) { + if (!mRemoveWithTaskOrganizer && mActivityOptions != null) { mRemoveWithTaskOrganizer = mActivityOptions.getRemoveWithTaskOranizer(); } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 8ad76a3eb327..79be9463aa05 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -1074,10 +1074,14 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { // Use launch-adjacent-flag-root if launching with launch-adjacent flag. if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0 && mLaunchAdjacentFlagRootTask != null) { - // If the adjacent launch is coming from the same root, launch to adjacent root instead. - if (sourceTask != null && mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment() != null + if (sourceTask != null && sourceTask == candidateTask) { + // Do nothing when task that is getting opened is same as the source. + } else if (sourceTask != null + && mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment() != null && (sourceTask == mLaunchAdjacentFlagRootTask || sourceTask.isDescendantOf(mLaunchAdjacentFlagRootTask))) { + // If the adjacent launch is coming from the same root, launch to + // adjacent root instead. return mLaunchAdjacentFlagRootTask.getAdjacentTaskFragment().asTask(); } else { return mLaunchAdjacentFlagRootTask; diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index d619547dbbd1..d780cae9e845 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -783,7 +783,8 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { } @Override - public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { enforceTaskPermission("createRootTask()"); final long origId = Binder.clearCallingIdentity(); try { @@ -795,7 +796,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { return; } - createRootTask(display, windowingMode, launchCookie); + createRootTask(display, windowingMode, launchCookie, removeWithTaskOrganizer); } } finally { Binder.restoreCallingIdentity(origId); @@ -804,6 +805,12 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { @VisibleForTesting Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie) { + return createRootTask(display, windowingMode, launchCookie, + false /* removeWithTaskOrganizer */); + } + + Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Create root task displayId=%d winMode=%d", display.mDisplayId, windowingMode); // We want to defer the task appear signal until the task is fully created and attached to @@ -816,6 +823,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { .setDeferTaskAppear(true) .setLaunchCookie(launchCookie) .setParent(display.getDefaultTaskDisplayArea()) + .setRemoveWithTaskOrganizer(removeWithTaskOrganizer) .build(); task.setDeferTaskAppear(false /* deferTaskAppear */); return task; diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 1d25dbc0d533..b2dab78b993d 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -371,7 +371,7 @@ class WallpaperController { boolean updateWallpaperOffset(WindowState wallpaperWin, boolean sync) { // Size of the display the wallpaper is rendered on. - final Rect lastWallpaperBounds = wallpaperWin.getLastReportedBounds(); + final Rect lastWallpaperBounds = wallpaperWin.getParentFrame(); // Full size of the wallpaper (usually larger than bounds above to parallax scroll when // swiping through Launcher pages). final Rect wallpaperFrame = wallpaperWin.getFrame(); diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index fb584feeaf04..8bdab9c22ab7 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -3197,11 +3197,11 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< private Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter, boolean isVoiceInteraction) { - if (isOrganized() + if (AppTransitionController.isTaskViewTask(this) || (isOrganized() // TODO(b/161711458): Clean-up when moved to shell. && getWindowingMode() != WINDOWING_MODE_FULLSCREEN && getWindowingMode() != WINDOWING_MODE_FREEFORM - && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW) { + && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW)) { return null; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 04699619bfc8..63607ad19f5a 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -3081,12 +3081,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mLastReportedConfiguration.getMergedConfiguration(); } - /** Returns the last window configuration bounds reported to the client. */ - Rect getLastReportedBounds() { - final Rect bounds = getLastReportedConfiguration().windowConfiguration.getBounds(); - return !bounds.isEmpty() ? bounds : getBounds(); - } - void adjustStartingWindowFlags() { if (mAttrs.type == TYPE_BASE_APPLICATION && mActivityRecord != null && mActivityRecord.mStartingWindow != null) { @@ -4421,6 +4415,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP pw.print("null"); } + if (mXOffset != 0 || mYOffset != 0) { + pw.println(prefix + "mXOffset=" + mXOffset + " mYOffset=" + mYOffset); + } if (mHScale != 1 || mVScale != 1) { pw.println(prefix + "mHScale=" + mHScale + " mVScale=" + mVScale); @@ -5573,7 +5570,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mSurfacePosition); if (mWallpaperScale != 1f) { - final Rect bounds = getLastReportedBounds(); + final Rect bounds = getParentFrame(); Matrix matrix = mTmpMatrix; matrix.setTranslate(mXOffset, mYOffset); matrix.postScale(mWallpaperScale, mWallpaperScale, bounds.exactCenterX(), @@ -5686,6 +5683,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP && imeTarget.compareTo(this) <= 0; return inTokenWithAndAboveImeTarget; } + + // The condition is for the system dialog not belonging to any Activity. + // (^FLAG_NOT_FOCUSABLE & FLAG_ALT_FOCUSABLE_IM) means the dialog is still focusable but + // should be placed above the IME window. + if ((mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)) + == FLAG_ALT_FOCUSABLE_IM && isTrustedOverlay() && canAddInternalSystemWindow()) { + return true; + } return false; } diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index f628fbad892d..abe48f894e59 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -464,6 +464,14 @@ </xs:complexType> <xs:complexType name="refreshRateConfigs"> + <xs:element name="defaultRefreshRate" type="xs:nonNegativeInteger" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> + <xs:element name="defaultPeakRefreshRate" type="xs:nonNegativeInteger" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> <xs:element name="lowerBlockingZoneConfigs" type="blockingZoneConfig" minOccurs="0" maxOccurs="1"> <xs:annotation name="final"/> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index cb081791ffce..2c97af55f092 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -186,8 +186,12 @@ package com.android.server.display.config { public class RefreshRateConfigs { ctor public RefreshRateConfigs(); + method public final java.math.BigInteger getDefaultPeakRefreshRate(); + method public final java.math.BigInteger getDefaultRefreshRate(); method public final com.android.server.display.config.BlockingZoneConfig getHigherBlockingZoneConfigs(); method public final com.android.server.display.config.BlockingZoneConfig getLowerBlockingZoneConfigs(); + method public final void setDefaultPeakRefreshRate(java.math.BigInteger); + method public final void setDefaultRefreshRate(java.math.BigInteger); method public final void setHigherBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig); method public final void setLowerBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig); } diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java index ca9ff6f15f3f..962a07ac6553 100644 --- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java +++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java @@ -43,23 +43,23 @@ import static com.android.server.backup.testing.Utils.transferStreamedData; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.intThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.intThat; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import static org.robolectric.shadow.api.Shadow.extract; @@ -2185,7 +2185,7 @@ public class KeyValueBackupTaskTest { task.waitCancel(); reset(transportMock.transport); taskFinished.block(); - verifyZeroInteractions(transportMock.transport); + verifyNoInteractions(transportMock.transport); } @Test diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java index dad9fe8648b2..31599eed539d 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java @@ -74,7 +74,7 @@ public class AudioDeviceBrokerTest { mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem)); mSpySystemServer = spy(new NoOpSystemServerAdapter()); mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory, - mSpySystemServer); + mSpySystemServer, mSpyAudioSystem); mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker); BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index 666d4010e921..3c735e335e75 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -41,7 +41,6 @@ import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.fingerprint.ISession; import android.hardware.biometrics.fingerprint.PointerContext; import android.hardware.fingerprint.Fingerprint; -import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.hardware.fingerprint.ISidefpsController; import android.hardware.fingerprint.IUdfpsOverlayController; @@ -55,7 +54,6 @@ import android.testing.TestableContext; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.internal.R; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.log.CallbackWithProbe; @@ -369,274 +367,6 @@ public class FingerprintAuthenticationClientTest { verify(mCancellationSignal).cancel(); } - @Test - public void fingerprintPowerIgnoresAuthInWindow() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - client.onPowerPressed(); - client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */, 2 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.moveTimeForward(1000); - mLooper.dispatchAll(); - - verify(mCallback).onClientFinished(any(), eq(false)); - verify(mCancellationSignal).cancel(); - } - - @Test - public void fingerprintAuthIgnoredWaitingForPower() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - client.onAuthenticated(new Fingerprint("friendly", 3 /* fingerId */, 4 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - client.onPowerPressed(); - mLooper.moveTimeForward(1000); - mLooper.dispatchAll(); - - verify(mCallback).onClientFinished(any(), eq(false)); - verify(mCancellationSignal).cancel(); - } - - @Test - public void fingerprintAuthFailsWhenAuthAfterPower() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - when(mHal.authenticate(anyLong())).thenReturn(mCancellationSignal); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - client.onPowerPressed(); - mLooper.dispatchAll(); - mLooper.moveTimeForward(1000); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - mLooper.moveTimeForward(1000); - mLooper.dispatchAll(); - - verify(mCallback, never()).onClientFinished(any(), eq(true)); - verify(mCallback).onClientFinished(any(), eq(false)); - when(mHal.authenticateWithContext(anyLong(), any())).thenReturn(mCancellationSignal); - } - - @Test - public void sideFingerprintDoesntSendAuthImmediately() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - } - - @Test - public void sideFingerprintSkipsWindowIfFingerUp() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - client.onAcquired(FINGER_UP, 0); - mLooper.dispatchAll(); - - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFingerprintSkipsWindowIfVendorMessageMatch() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - final int vendorAcquireMessage = 1234; - - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, - FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR); - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage, - vendorAcquireMessage); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, vendorAcquireMessage); - mLooper.dispatchAll(); - - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFingerprintDoesNotSkipWindowOnVendorErrorMismatch() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - final int vendorAcquireMessage = 1234; - - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, - FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR); - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerVendorAcquireMessage, - vendorAcquireMessage); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_VENDOR, 1); - mLooper.dispatchAll(); - - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - } - - @Test - public void sideFingerprintSendsAuthIfFingerUp() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAcquired(FINGER_UP, 0); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFingerprintShortCircuitExpires() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - - final int timeBeforeAuthSent = 500; - - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsKeyguardPowerPressWindow, timeBeforeAuthSent); - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsSkipWaitForPowerAcquireMessage, FINGER_UP); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - mLooper.dispatchAll(); - client.onAcquired(FINGER_UP, 0); - mLooper.dispatchAll(); - - mLooper.moveTimeForward(500); - mLooper.dispatchAll(); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - - mLooper.moveTimeForward(500); - mLooper.dispatchAll(); - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFingerprintPowerWindowStartsOnAcquireStart() throws Exception { - final int powerWindow = 500; - final long authStart = 300; - - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsBpPowerPressWindow, powerWindow); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - - // Acquire start occurs at time = 0ms - when(mClock.millis()).thenReturn(0L); - client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); - - // Auth occurs at time = 300 - when(mClock.millis()).thenReturn(authStart); - // At this point the delay should be 500 - (300 - 0) == 200 milliseconds. - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - - // After waiting 200 milliseconds, auth should succeed. - mLooper.moveTimeForward(powerWindow - authStart); - mLooper.dispatchAll(); - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFingerprintPowerWindowStartsOnLastAcquireStart() throws Exception { - final int powerWindow = 500; - - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - mContext.getOrCreateTestableResources().addOverride( - R.integer.config_sidefpsBpPowerPressWindow, powerWindow); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - // Acquire start occurs at time = 0ms - when(mClock.millis()).thenReturn(0L); - client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); - - // Auth reject occurs at time = 300ms - when(mClock.millis()).thenReturn(300L); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - false /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - - mLooper.moveTimeForward(300); - mLooper.dispatchAll(); - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - - when(mClock.millis()).thenReturn(1300L); - client.onAcquired(FingerprintManager.FINGERPRINT_ACQUIRED_START, 0 /* vendorCode */); - - // If code is correct, the new acquired start timestamp should be used - // and the code should only have to wait 500 - (1500-1300)ms. - when(mClock.millis()).thenReturn(1500L); - client.onAuthenticated(new Fingerprint("friendly", 4 /* fingerId */, 5 /* deviceId */), - true /* authenticated */, new ArrayList<>()); - mLooper.dispatchAll(); - - mLooper.moveTimeForward(299); - mLooper.dispatchAll(); - verify(mCallback, never()).onClientFinished(any(), anyBoolean()); - - mLooper.moveTimeForward(1); - mLooper.dispatchAll(); - verify(mCallback).onClientFinished(any(), eq(true)); - } - - @Test - public void sideFpsPowerPressCancelsIsntantly() throws Exception { - when(mSensorProps.isAnySidefpsType()).thenReturn(true); - - final FingerprintAuthenticationClient client = createClient(1); - client.start(mCallback); - - client.onPowerPressed(); - mLooper.dispatchAll(); - - verify(mCallback, never()).onClientFinished(any(), eq(true)); - verify(mCallback).onClientFinished(any(), eq(false)); - } - private FingerprintAuthenticationClient createClient() throws RemoteException { return createClient(100 /* version */, true /* allowBackgroundAuthentication */); } diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java index 86c59379a61e..77e5d1d60cb5 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -51,6 +51,8 @@ import java.nio.file.Path; public final class DisplayDeviceConfigTest { private static final int DEFAULT_PEAK_REFRESH_RATE = 75; private static final int DEFAULT_REFRESH_RATE = 120; + private static final int DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE = 55; + private static final int DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE = 95; private static final int[] LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{10, 30}; private static final int[] LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{1, 21}; private static final int[] HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{160}; @@ -150,8 +152,10 @@ public final class DisplayDeviceConfigTest { assertEquals("ProximitySensor123", mDisplayDeviceConfig.getProximitySensor().name); assertEquals("prox_type_1", mDisplayDeviceConfig.getProximitySensor().type); - assertEquals(75, mDisplayDeviceConfig.getDefaultLowRefreshRate()); - assertEquals(90, mDisplayDeviceConfig.getDefaultHighRefreshRate()); + assertEquals(75, mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate()); + assertEquals(90, mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate()); + assertEquals(85, mDisplayDeviceConfig.getDefaultPeakRefreshRate()); + assertEquals(45, mDisplayDeviceConfig.getDefaultRefreshRate()); assertArrayEquals(new int[]{45, 55}, mDisplayDeviceConfig.getLowDisplayBrightnessThresholds()); assertArrayEquals(new int[]{50, 60}, @@ -230,8 +234,12 @@ public final class DisplayDeviceConfigTest { mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle(), ZERO_DELTA); assertArrayEquals(new float[]{29, 30, 31}, mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle(), ZERO_DELTA); - assertEquals(mDisplayDeviceConfig.getDefaultLowRefreshRate(), DEFAULT_REFRESH_RATE); - assertEquals(mDisplayDeviceConfig.getDefaultHighRefreshRate(), DEFAULT_PEAK_REFRESH_RATE); + assertEquals(mDisplayDeviceConfig.getDefaultLowBlockingZoneRefreshRate(), + DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE); + assertEquals(mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate(), + DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE); + assertEquals(mDisplayDeviceConfig.getDefaultPeakRefreshRate(), DEFAULT_PEAK_REFRESH_RATE); + assertEquals(mDisplayDeviceConfig.getDefaultRefreshRate(), DEFAULT_REFRESH_RATE); assertArrayEquals(mDisplayDeviceConfig.getLowDisplayBrightnessThresholds(), LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); assertArrayEquals(mDisplayDeviceConfig.getLowAmbientBrightnessThresholds(), @@ -449,6 +457,8 @@ public final class DisplayDeviceConfigTest { + "<type>prox_type_1</type>\n" + "</proxSensor>\n" + "<refreshRate>\n" + + "<defaultRefreshRate>45</defaultRefreshRate>\n" + + "<defaultPeakRefreshRate>85</defaultPeakRefreshRate>\n" + "<lowerBlockingZoneConfigs>\n" + "<defaultRefreshRate>75</defaultRefreshRate>\n" + "<blockingZoneThreshold>\n" @@ -550,10 +560,14 @@ public final class DisplayDeviceConfigTest { .thenReturn(new int[]{370, 380, 390}); // Configs related to refresh rates and blocking zones - when(mResources.getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate)) + when(mResources.getInteger(R.integer.config_defaultPeakRefreshRate)) .thenReturn(DEFAULT_PEAK_REFRESH_RATE); - when(mResources.getInteger(com.android.internal.R.integer.config_defaultRefreshRate)) + when(mResources.getInteger(R.integer.config_defaultRefreshRate)) .thenReturn(DEFAULT_REFRESH_RATE); + when(mResources.getInteger(R.integer.config_fixedRefreshRateInHighZone)) + .thenReturn(DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE); + when(mResources.getInteger(R.integer.config_defaultRefreshRateInZone)) + .thenReturn(DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE); when(mResources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate)) .thenReturn(LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE); when(mResources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate)) diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java index b133a2a44fcf..af39dd44065e 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java @@ -1869,6 +1869,10 @@ public class DisplayModeDirectorTest { .thenReturn(75); when(resources.getInteger(R.integer.config_defaultRefreshRate)) .thenReturn(45); + when(resources.getInteger(R.integer.config_fixedRefreshRateInHighZone)) + .thenReturn(65); + when(resources.getInteger(R.integer.config_defaultRefreshRateInZone)) + .thenReturn(85); when(resources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate)) .thenReturn(new int[]{5}); when(resources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate)) @@ -1888,6 +1892,8 @@ public class DisplayModeDirectorTest { assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 45, 0.0); assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 75, 0.0); + assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 65); + assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 85); assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(), new int[]{250}); assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(), @@ -1899,17 +1905,21 @@ public class DisplayModeDirectorTest { // Notify that the default display is updated, such that DisplayDeviceConfig has new values DisplayDeviceConfig displayDeviceConfig = mock(DisplayDeviceConfig.class); - when(displayDeviceConfig.getDefaultLowRefreshRate()).thenReturn(50); - when(displayDeviceConfig.getDefaultHighRefreshRate()).thenReturn(55); + when(displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50); + when(displayDeviceConfig.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55); + when(displayDeviceConfig.getDefaultRefreshRate()).thenReturn(60); + when(displayDeviceConfig.getDefaultPeakRefreshRate()).thenReturn(65); when(displayDeviceConfig.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25}); when(displayDeviceConfig.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30}); when(displayDeviceConfig.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210}); when(displayDeviceConfig.getHighAmbientBrightnessThresholds()).thenReturn(new int[]{2100}); director.defaultDisplayDeviceUpdated(displayDeviceConfig); - assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 50, 0.0); - assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 55, + assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0); + assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 65, 0.0); + assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 55); + assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 50); assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(), new int[]{210}); assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(), @@ -1922,6 +1932,8 @@ public class DisplayModeDirectorTest { // Notify that the default display is updated, such that DeviceConfig has new values FakeDeviceConfig config = mInjector.getDeviceConfig(); config.setDefaultPeakRefreshRate(60); + config.setRefreshRateInHighZone(65); + config.setRefreshRateInLowZone(70); config.setLowAmbientBrightnessThresholds(new int[]{20}); config.setLowDisplayBrightnessThresholds(new int[]{10}); config.setHighDisplayBrightnessThresholds(new int[]{255}); @@ -1929,9 +1941,11 @@ public class DisplayModeDirectorTest { director.defaultDisplayDeviceUpdated(displayDeviceConfig); - assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 50, 0.0); + assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0); assertEquals(director.getSettingsObserver().getDefaultPeakRefreshRate(), 60, 0.0); + assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 65); + assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 70); assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(), new int[]{255}); assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(), @@ -1971,8 +1985,8 @@ public class DisplayModeDirectorTest { any(Handler.class)); DisplayDeviceConfig ddcMock = mock(DisplayDeviceConfig.class); - when(ddcMock.getDefaultLowRefreshRate()).thenReturn(50); - when(ddcMock.getDefaultHighRefreshRate()).thenReturn(55); + when(ddcMock.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50); + when(ddcMock.getDefaultHighBlockingZoneRefreshRate()).thenReturn(55); when(ddcMock.getLowDisplayBrightnessThresholds()).thenReturn(new int[]{25}); when(ddcMock.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30}); when(ddcMock.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210}); diff --git a/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java b/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java new file mode 100644 index 000000000000..24fc34849829 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/display/HbmEventTest.java @@ -0,0 +1,57 @@ +/* + * 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.display; + +import static org.junit.Assert.assertEquals; + + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class HbmEventTest { + private long mStartTimeMillis; + private long mEndTimeMillis; + private HbmEvent mHbmEvent; + + @Before + public void setUp() { + mStartTimeMillis = 10; + mEndTimeMillis = 20; + mHbmEvent = new HbmEvent(mStartTimeMillis, mEndTimeMillis); + } + + @Test + public void getCorrectValues() { + assertEquals(mHbmEvent.getStartTimeMillis(), mStartTimeMillis); + assertEquals(mHbmEvent.getEndTimeMillis(), mEndTimeMillis); + } + + @Test + public void toStringGeneratesExpectedString() { + String actualString = mHbmEvent.toString(); + String expectedString = "HbmEvent: {startTimeMillis:" + mStartTimeMillis + + ", endTimeMillis: " + mEndTimeMillis + "}, total: " + + ((mEndTimeMillis - mStartTimeMillis) / 1000) + "]"; + assertEquals(actualString, expectedString); + } +} diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java index 53fa3e2db376..da2e1be00769 100644 --- a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java @@ -27,9 +27,7 @@ import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIG import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED; import static com.android.server.display.AutomaticBrightnessController .AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE; - import static com.android.server.display.DisplayDeviceConfig.HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT; - import static com.android.server.display.HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID; import static org.junit.Assert.assertEquals; @@ -102,6 +100,7 @@ public class HighBrightnessModeControllerTest { private Binder mDisplayToken; private String mDisplayUniqueId; private Context mContextSpy; + private HighBrightnessModeMetadata mHighBrightnessModeMetadata; @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); @@ -124,6 +123,7 @@ public class HighBrightnessModeControllerTest { mTestLooper = new TestLooper(mClock::now); mDisplayToken = null; mDisplayUniqueId = "unique_id"; + mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); final MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContextSpy); when(mContextSpy.getContentResolver()).thenReturn(resolver); @@ -140,7 +140,8 @@ public class HighBrightnessModeControllerTest { initHandler(null); final HighBrightnessModeController hbmc = new HighBrightnessModeController( mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken, - mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, mContextSpy); + mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, + null, mContextSpy); assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF); assertEquals(hbmc.getTransitionPoint(), HBM_TRANSITION_POINT_INVALID, 0.0f); } @@ -150,7 +151,8 @@ public class HighBrightnessModeControllerTest { initHandler(null); final HighBrightnessModeController hbmc = new HighBrightnessModeController( mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken, - mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, mContextSpy); + mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, null, null, () -> {}, + null, mContextSpy); hbmc.setAutoBrightnessEnabled(AUTO_BRIGHTNESS_ENABLED); hbmc.onAmbientLuxChange(MINIMUM_LUX - 1); // below allowed range assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF); @@ -705,9 +707,12 @@ public class HighBrightnessModeControllerTest { // Creates instance with standard initialization values. private HighBrightnessModeController createDefaultHbm(OffsettableClock clock) { initHandler(clock); + if (mHighBrightnessModeMetadata == null) { + mHighBrightnessModeMetadata = new HighBrightnessModeMetadata(); + } return new HighBrightnessModeController(mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken, mDisplayUniqueId, DEFAULT_MIN, DEFAULT_MAX, - DEFAULT_HBM_DATA, null, () -> {}, mContextSpy); + DEFAULT_HBM_DATA, null, () -> {}, mHighBrightnessModeMetadata, mContextSpy); } private void initHandler(OffsettableClock clock) { diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java new file mode 100644 index 000000000000..ede54e096ad0 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeMetadataTest.java @@ -0,0 +1,59 @@ +/* + * 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.display; + +import static org.junit.Assert.assertEquals; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class HighBrightnessModeMetadataTest { + private HighBrightnessModeMetadata mHighBrightnessModeMetadata; + + private long mRunningStartTimeMillis = -1; + + @Before + public void setUp() { + mHighBrightnessModeMetadata = new HighBrightnessModeMetadata(); + } + + @Test + public void checkDefaultValues() { + assertEquals(mHighBrightnessModeMetadata.getRunningStartTimeMillis(), + mRunningStartTimeMillis); + assertEquals(mHighBrightnessModeMetadata.getHbmEventQueue().size(), 0); + } + + @Test + public void checkSetValues() { + mRunningStartTimeMillis = 10; + mHighBrightnessModeMetadata.setRunningStartTimeMillis(mRunningStartTimeMillis); + assertEquals(mHighBrightnessModeMetadata.getRunningStartTimeMillis(), + mRunningStartTimeMillis); + HbmEvent expectedHbmEvent = new HbmEvent(10, 20); + mHighBrightnessModeMetadata.addHbmEvent(expectedHbmEvent); + HbmEvent actualHbmEvent = mHighBrightnessModeMetadata.getHbmEventQueue().peekFirst(); + assertEquals(expectedHbmEvent.toString(), actualHbmEvent.toString()); + } +} diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java index 638637d544de..6790ad9a5da1 100644 --- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -18,6 +18,7 @@ package com.android.server.display; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.DEFAULT_DISPLAY_GROUP; +import static android.view.Display.TYPE_EXTERNAL; import static android.view.Display.TYPE_INTERNAL; import static android.view.Display.TYPE_VIRTUAL; @@ -173,7 +174,7 @@ public class LogicalDisplayMapperTest { @Test public void testDisplayDeviceAddAndRemove_NonInternalTypes() { - testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_EXTERNAL); + testDisplayDeviceAddAndRemove_NonInternal(TYPE_EXTERNAL); testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_WIFI); testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_OVERLAY); testDisplayDeviceAddAndRemove_NonInternal(TYPE_VIRTUAL); @@ -218,7 +219,7 @@ public class LogicalDisplayMapperTest { @Test public void testDisplayDeviceAddAndRemove_OneExternalDefault() { - DisplayDevice device = createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800, + DisplayDevice device = createDisplayDevice(TYPE_EXTERNAL, 600, 800, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); // add @@ -268,7 +269,7 @@ public class LogicalDisplayMapperTest { public void testGetDisplayIdsLocked() { add(createDisplayDevice(TYPE_INTERNAL, 600, 800, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - add(createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800, 0)); + add(createDisplayDevice(TYPE_EXTERNAL, 600, 800, 0)); add(createDisplayDevice(TYPE_VIRTUAL, 600, 800, 0)); int [] ids = mLogicalDisplayMapper.getDisplayIdsLocked(Process.SYSTEM_UID, @@ -460,7 +461,7 @@ public class LogicalDisplayMapperTest { Layout layout = new Layout(); layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address, true, true); - layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, false, false); + layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address, false, true); when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(layout); layout = new Layout(); @@ -469,6 +470,8 @@ public class LogicalDisplayMapperTest { when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(layout); when(mDeviceStateToLayoutMapSpy.get(2)).thenReturn(layout); + when(mDeviceStateToLayoutMapSpy.size()).thenReturn(4); + LogicalDisplay display1 = add(device1); assertEquals(info(display1).address, info(device1).address); assertEquals(DEFAULT_DISPLAY, id(display1)); @@ -481,8 +484,15 @@ public class LogicalDisplayMapperTest { mLogicalDisplayMapper.setDeviceStateLocked(0, false); mLooper.moveTimeForward(1000); mLooper.dispatchAll(); + // The new state is not applied until the boot is completed assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked()); assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked()); + + mLogicalDisplayMapper.onBootCompleted(); + mLooper.moveTimeForward(1000); + mLooper.dispatchAll(); + assertTrue(mLogicalDisplayMapper.getDisplayLocked(device1).isEnabledLocked()); + assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked()); assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isInTransitionLocked()); assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isInTransitionLocked()); @@ -623,6 +633,23 @@ public class LogicalDisplayMapperTest { assertEquals(3, threeDisplaysEnabled.length); } + @Test + public void testCreateNewLogicalDisplay() { + DisplayDevice device1 = createDisplayDevice(TYPE_EXTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + when(mDeviceStateToLayoutMapSpy.size()).thenReturn(1); + LogicalDisplay display1 = add(device1); + + assertTrue(display1.isEnabledLocked()); + + DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + when(mDeviceStateToLayoutMapSpy.size()).thenReturn(2); + LogicalDisplay display2 = add(device2); + + assertFalse(display2.isEnabledLocked()); + } + ///////////////// // Helper Methods ///////////////// diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index 06a79f47de55..1407cdd8600c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -391,7 +391,7 @@ public class WallpaperControllerTests extends WindowTestsBase { dc.updateOrientation(); dc.sendNewConfiguration(); spyOn(wallpaperWindow); - doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getLastReportedBounds(); + doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getParentFrame(); } @Test 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 0568f2acf366..3556ded23351 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -42,6 +42,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; +import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; @@ -969,6 +970,19 @@ public class WindowStateTests extends WindowTestsBase { assertFalse(sameTokenWindow.needsRelativeLayeringToIme()); } + @UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD}) + @Test + public void testNeedsRelativeLayeringToIme_systemDialog() { + WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, + mDisplayContent, + "SystemDialog", true); + mDisplayContent.setImeLayeringTarget(mAppWindow); + mAppWindow.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + makeWindowVisible(mImeWindow); + systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; + assertTrue(systemDialogWindow.needsRelativeLayeringToIme()); + } + @Test public void testSetFreezeInsetsState() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java index 77fca451547d..7959d82ae22f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; 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.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; @@ -31,6 +32,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; +import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; @@ -543,4 +545,28 @@ public class ZOrderingTests extends WindowTestsBase { assertZOrderGreaterThan(mTransaction, popupWindow.getSurfaceControl(), mDisplayContent.getImeContainer().getSurfaceControl()); } + + @Test + public void testSystemDialogWindow_expectHigherThanIme_inMultiWindow() { + // Simulate the app window is in multi windowing mode and being IME target + mAppWindow.getConfiguration().windowConfiguration.setWindowingMode( + WINDOWING_MODE_MULTI_WINDOW); + mDisplayContent.setImeLayeringTarget(mAppWindow); + mDisplayContent.setImeInputTarget(mAppWindow); + makeWindowVisible(mImeWindow); + + // Create a popupWindow + final WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, + mDisplayContent, "SystemDialog", true); + systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; + spyOn(systemDialogWindow); + + mDisplayContent.assignChildLayers(mTransaction); + + // Verify the surface layer of the popupWindow should higher than IME + verify(systemDialogWindow).needsRelativeLayeringToIme(); + assertThat(systemDialogWindow.needsRelativeLayeringToIme()).isTrue(); + assertZOrderGreaterThan(mTransaction, systemDialogWindow.getSurfaceControl(), + mDisplayContent.getImeContainer().getSurfaceControl()); + } } |