diff options
277 files changed, 6565 insertions, 3405 deletions
diff --git a/config/preloaded-classes-denylist b/config/preloaded-classes-denylist index e3e929cb00d9..a6a1d1680b7b 100644 --- a/config/preloaded-classes-denylist +++ b/config/preloaded-classes-denylist @@ -1,5 +1,4 @@ android.content.AsyncTaskLoader$LoadTask -android.media.MediaCodecInfo$CodecCapabilities$FeatureList android.net.ConnectivityThread$Singleton android.os.FileObserver android.os.NullVibrator diff --git a/core/api/current.txt b/core/api/current.txt index 6707c15de682..b7f7a7f9e779 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -16896,6 +16896,7 @@ package android.graphics { method public android.graphics.Paint.FontMetricsInt getFontMetricsInt(); method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void getFontMetricsIntForLocale(@NonNull android.graphics.Paint.FontMetricsInt); method public float getFontSpacing(); + method @FlaggedApi("com.android.text.flags.typeface_redesign_readonly") @Nullable public String getFontVariationOverride(); method public String getFontVariationSettings(); method public int getHinting(); method public float getLetterSpacing(); @@ -16974,6 +16975,7 @@ package android.graphics { method public void setFilterBitmap(boolean); method public void setFlags(int); method public void setFontFeatureSettings(String); + method @FlaggedApi("com.android.text.flags.typeface_redesign_readonly") public boolean setFontVariationOverride(@Nullable String); method public boolean setFontVariationSettings(String); method public void setHinting(int); method public void setLetterSpacing(float); diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 999db18a1229..6151b8e2ef0a 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -142,6 +142,15 @@ public abstract class ActivityManagerInternal { String processName, String abiOverride, int uid, Runnable crashHandler); /** + * Called when a user is being deleted. This can happen during normal device usage + * or just at startup, when partially removed users are purged. Any state persisted by the + * ActivityManager should be purged now. + * + * @param userId The user being cleaned up. + */ + public abstract void onUserRemoving(@UserIdInt int userId); + + /** * Called when a user has been deleted. This can happen during normal device usage * or just at startup, when partially removed users are purged. Any state persisted by the * ActivityManager should be purged now. diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index 3d85ea6a1fca..ffd235f91e09 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -1129,6 +1129,10 @@ public final class LoadedApk { @UnsupportedAppUsage public ClassLoader getClassLoader() { + ClassLoader ret = mClassLoader; + if (ret != null) { + return ret; + } synchronized (mLock) { if (mClassLoader == null) { createOrUpdateClassLoaderLocked(null /*addedPaths*/); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 252978facac0..c2ce7d511681 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5991,6 +5991,15 @@ public class Notification implements Parcelable } setHeaderlessVerticalMargins(contentView, p, hasSecondLine); + // Update margins to leave space for the top line (but not for headerless views like + // HUNS, which use a different layout that already accounts for that). + if (Flags.notificationsRedesignTemplates() && !p.mHeaderless) { + int margin = getContentMarginTop(mContext, + R.dimen.notification_2025_content_margin_top); + contentView.setViewLayoutMargin(R.id.notification_main_column, + RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX); + } + return contentView; } @@ -6463,16 +6472,6 @@ public class Notification implements Parcelable big.setColorStateList(R.id.snooze_button, "setImageTintList", actionColor); big.setColorStateList(R.id.bubble_button, "setImageTintList", actionColor); - // Update margins to leave space for the top line (but not for HUNs, which use a - // different layout that already accounts for that). - if (Flags.notificationsRedesignTemplates() - && p.mViewType != StandardTemplateParams.VIEW_TYPE_HEADS_UP) { - int margin = getContentMarginTop(mContext, - R.dimen.notification_2025_content_margin_top); - big.setViewLayoutMargin(R.id.notification_main_column, RemoteViews.MARGIN_TOP, - margin, TypedValue.COMPLEX_UNIT_PX); - } - boolean validRemoteInput = false; // In the UI, contextual actions appear separately from the standard actions, so we diff --git a/core/java/android/app/backup/BackupManagerInternal.java b/core/java/android/app/backup/BackupManagerInternal.java new file mode 100644 index 000000000000..ceb5ae01f177 --- /dev/null +++ b/core/java/android/app/backup/BackupManagerInternal.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +import android.annotation.UserIdInt; +import android.os.IBinder; + +/** + * Local system service interface for {@link com.android.server.backup.BackupManagerService}. + * + * @hide Only for use within the system server. + */ +public interface BackupManagerInternal { + + /** + * Notifies the Backup Manager Service that an agent has become available. This + * method is only invoked by the Activity Manager. + */ + void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent); + + /** + * Notify the Backup Manager Service that an agent has unexpectedly gone away. + * This method is only invoked by the Activity Manager. + */ + void agentDisconnectedForUser(String packageName, @UserIdInt int userId); +} diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index 041c2a7c09f4..5d01d72c35a0 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -93,38 +93,6 @@ interface IBackupManager { IBackupObserver observer); /** - * Notifies the Backup Manager Service that an agent has become available. This - * method is only invoked by the Activity Manager. - * - * If {@code userId} is different from the calling user id, then the caller must hold the - * android.permission.INTERACT_ACROSS_USERS_FULL permission. - * - * @param userId User id for which an agent has become available. - */ - void agentConnectedForUser(int userId, String packageName, IBinder agent); - - /** - * {@link android.app.backup.IBackupManager.agentConnected} for the calling user id. - */ - void agentConnected(String packageName, IBinder agent); - - /** - * Notify the Backup Manager Service that an agent has unexpectedly gone away. - * This method is only invoked by the Activity Manager. - * - * If {@code userId} is different from the calling user id, then the caller must hold the - * android.permission.INTERACT_ACROSS_USERS_FULL permission. - * - * @param userId User id for which an agent has unexpectedly gone away. - */ - void agentDisconnectedForUser(int userId, String packageName); - - /** - * {@link android.app.backup.IBackupManager.agentDisconnected} for the calling user id. - */ - void agentDisconnected(String packageName); - - /** * Notify the Backup Manager Service that an application being installed will * need a data-restore pass. This method is only invoked by the Package Manager. * diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java index 316d129bd6b9..971d402569c0 100644 --- a/core/java/android/companion/CompanionDeviceService.java +++ b/core/java/android/companion/CompanionDeviceService.java @@ -62,10 +62,11 @@ import java.util.concurrent.Executor; * * <p> * If the companion application has requested observing device presence (see - * {@link CompanionDeviceManager#startObservingDevicePresence(String)}) the system will - * <a href="https://developer.android.com/guide/components/bound-services"> bind the service</a> - * when it detects the device nearby (for BLE devices) or when the device is connected - * (for Bluetooth devices). + * {@link CompanionDeviceManager#stopObservingDevicePresence(ObservingDevicePresenceRequest)}) + * the system will <a href="https://developer.android.com/guide/components/bound-services"> + * bind the service</a> when one of the {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified. * * <p> * The system binding {@link CompanionDeviceService} elevates the priority of the process that @@ -102,15 +103,25 @@ public abstract class CompanionDeviceService extends Service { /** * An intent action for a service to be bound whenever this app's companion device(s) - * are nearby. + * are nearby or self-managed device(s) report app appeared. * - * <p>The app will be kept alive for as long as the device is nearby or companion app reports - * appeared. - * If the app is not running at the time device gets connected, the app will be woken up.</p> + * <p>The app will be kept bound by the system when one of the + * {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified. * - * <p>Shortly after the device goes out of range or the companion app reports disappeared, - * the service will be unbound, and the app will be eligible for cleanup, unless any other - * user-visible components are running.</p> + * If the app is not running when one of the + * {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified, the app will be + * kept bound by the system.</p> + * + * <p>Shortly, the service will be unbound if both + * {@link DevicePresenceEvent#EVENT_BLE_DISAPPEARED} and + * {@link DevicePresenceEvent#EVENT_BT_DISCONNECTED} are notified, or + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_DISAPPEARED} event is notified. + * The app will be eligible for cleanup, unless any other user-visible components are + * running.</p> * * If running in background is not essential for the devices that this app can manage, * app should avoid declaring this service.</p> diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java index e271cf4f60ec..4e292d0b7d18 100644 --- a/core/java/android/content/ClipData.java +++ b/core/java/android/content/ClipData.java @@ -221,6 +221,12 @@ public class ClipData implements Parcelable { // if the data is obtained from {@link #copyForTransferWithActivityInfo} private ActivityInfo mActivityInfo; + private boolean mTokenVerificationEnabled; + + void setTokenVerificationEnabled() { + mTokenVerificationEnabled = true; + } + /** * A builder for a ClipData Item. */ @@ -398,7 +404,9 @@ public class ClipData implements Parcelable { * Retrieve the raw Intent contained in this Item. */ public Intent getIntent() { - Intent.maybeMarkAsMissingCreatorToken(mIntent); + if (mTokenVerificationEnabled) { + Intent.maybeMarkAsMissingCreatorToken(mIntent); + } return mIntent; } @@ -1353,6 +1361,12 @@ public class ClipData implements Parcelable { } } + void setTokenVerificationEnabled() { + for (int i = 0; i < mItems.size(); ++i) { + mItems.get(i).setTokenVerificationEnabled(); + } + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 350048df3112..01e24d81a7cd 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -12506,6 +12506,9 @@ public class Intent implements Parcelable, Cloneable { if (intent.mExtras != null) { intent.mExtras.enableTokenVerification(); } + if (intent.mClipData != null) { + intent.mClipData.setTokenVerificationEnabled(); + } }; /** @hide */ @@ -12517,6 +12520,9 @@ public class Intent implements Parcelable, Cloneable { // mark trusted creator token present. mExtras.enableTokenVerification(); } + if (mClipData != null) { + mClipData.setTokenVerificationEnabled(); + } } /** @hide */ diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index c16582f19c9b..8c7e93a834b7 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -4649,6 +4649,7 @@ public abstract class PackageManager { * the Android Keystore backed by an isolated execution environment. The version indicates * which features are implemented in the isolated execution environment: * <ul> + * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record. * <li>300: Ability to include a second IMEI in the ID attestation record, see * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}. * <li>200: Hardware support for Curve 25519 (including both Ed25519 signature generation and @@ -4682,6 +4683,7 @@ public abstract class PackageManager { * StrongBox</a>. If this feature has a version, the version number indicates which features are * implemented in StrongBox: * <ul> + * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record. * <li>300: Ability to include a second IMEI in the ID attestation record, see * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}. * <li>200: No new features for StrongBox (the Android Keystore environment backed by an diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java index d84d29292ed5..8fbe05c4e9eb 100644 --- a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java +++ b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java @@ -121,15 +121,20 @@ public class FingerprintSensorPropertiesInternal extends SensorPropertiesInterna /** * Returns if sensor type is ultrasonic Udfps - * @return true if sensor is ultrasonic Udfps, false otherwise */ public boolean isUltrasonicUdfps() { return sensorType == TYPE_UDFPS_ULTRASONIC; } /** + * Returns if sensor type is optical Udfps + */ + public boolean isOpticalUdfps() { + return sensorType == TYPE_UDFPS_OPTICAL; + } + + /** * Returns if sensor type is side-FPS - * @return true if sensor is side-fps, false otherwise */ public boolean isAnySidefpsType() { switch (sensorType) { diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index ed510e467f82..2bb28a1b6b0b 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -266,6 +266,11 @@ interface IInputManager { @PermissionManuallyEnforced @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.MANAGE_KEY_GESTURES)") + AidlInputGestureData getInputGesture(int userId, in AidlInputGestureData.Trigger trigger); + + @PermissionManuallyEnforced + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MANAGE_KEY_GESTURES)") int addCustomInputGesture(int userId, in AidlInputGestureData data); @PermissionManuallyEnforced diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 10224c1be788..cf41e138047a 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1480,6 +1480,30 @@ public final class InputManager { mGlobal.unregisterKeyGestureEventHandler(handler); } + /** + * Find an input gesture mapped to a particular trigger. + * + * @param trigger to find the input gesture for + * @return input gesture mapped to the provided trigger, {@code null} if none found + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) + @UserHandleAware + @Nullable + public InputGestureData getInputGesture(@NonNull InputGestureData.Trigger trigger) { + try { + AidlInputGestureData result = mIm.getInputGesture(mContext.getUserId(), + trigger.getAidlTrigger()); + if (result == null) { + return null; + } + return new InputGestureData(result); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** Adds a new custom input gesture * * @param inputGestureData gesture data to add as custom gesture diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index cb1e0161441f..4025242fd208 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -43,6 +43,9 @@ public final class KeyGestureEvent { private static final int LOG_EVENT_UNSPECIFIED = FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED; + // Used as a placeholder to identify if a gesture is reserved for system + public static final int KEY_GESTURE_TYPE_SYSTEM_RESERVED = -1; + // These values should not change and values should not be re-used as this data is persisted to // long term storage and must be kept backwards compatible. public static final int KEY_GESTURE_TYPE_UNSPECIFIED = 0; @@ -144,6 +147,7 @@ public final class KeyGestureEvent { public static final int ACTION_GESTURE_COMPLETE = 2; @IntDef(prefix = "KEY_GESTURE_TYPE_", value = { + KEY_GESTURE_TYPE_SYSTEM_RESERVED, KEY_GESTURE_TYPE_UNSPECIFIED, KEY_GESTURE_TYPE_HOME, KEY_GESTURE_TYPE_RECENT_APPS, @@ -232,6 +236,26 @@ public final class KeyGestureEvent { public @interface KeyGestureType { } + /** + * Returns whether the key gesture type passed as argument is allowed for visible background + * users. + * + * @hide + */ + public static boolean isVisibleBackgrounduserAllowedGesture(int keyGestureType) { + switch (keyGestureType) { + case KEY_GESTURE_TYPE_SLEEP: + case KEY_GESTURE_TYPE_WAKEUP: + case KEY_GESTURE_TYPE_LAUNCH_ASSISTANT: + case KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT: + case KEY_GESTURE_TYPE_VOLUME_MUTE: + case KEY_GESTURE_TYPE_RECENT_APPS: + case KEY_GESTURE_TYPE_APP_SWITCH: + return false; + } + return true; + } + public KeyGestureEvent(@NonNull AidlKeyGestureEvent keyGestureEvent) { this.mKeyGestureEvent = keyGestureEvent; } @@ -645,6 +669,8 @@ public final class KeyGestureEvent { private static String keyGestureTypeToString(@KeyGestureType int value) { switch (value) { + case KEY_GESTURE_TYPE_SYSTEM_RESERVED: + return "KEY_GESTURE_TYPE_SYSTEM_RESERVED"; case KEY_GESTURE_TYPE_UNSPECIFIED: return "KEY_GESTURE_TYPE_UNSPECIFIED"; case KEY_GESTURE_TYPE_HOME: diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 2bc6ab5a18e9..d5640221e6bd 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -2783,4 +2783,12 @@ public final class Debug */ public static native boolean logAllocatorStats(); + /** + * Return the amount of memory (in kB) allocated by kernel drivers through CMA. + * @return a non-negative value or -1 on error. + * + * @hide + */ + public static native long getKernelCmaUsageKb(); + } diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java index 12080ca511b2..45a7afa014a1 100644 --- a/core/java/android/os/GraphicsEnvironment.java +++ b/core/java/android/os/GraphicsEnvironment.java @@ -706,11 +706,11 @@ public class GraphicsEnvironment { * @param context */ public void showAngleInUseDialogBox(Context context) { - if (!shouldShowAngleInUseDialogBox(context)) { + if (!mShouldUseAngle) { return; } - if (!mShouldUseAngle) { + if (!shouldShowAngleInUseDialogBox(context)) { return; } diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index e24f08b7dfe5..8b8369890d1b 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -110,6 +110,14 @@ flag { } flag { + name: "allow_thermal_hal_skin_forecast" + is_exported: true + namespace: "game" + description: "Enable thermal HAL skin temperature forecast to be used by headroom API" + bug: "383211885" +} + +flag { name: "allow_thermal_headroom_thresholds" is_exported: true namespace: "game" diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 11dddfb24ad5..baaaa464a4cf 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -10525,6 +10525,15 @@ public final class Settings { public static final String SCREENSAVER_ACTIVATE_ON_SLEEP = "screensaver_activate_on_sleep"; /** + * If screensavers are enabled, whether the screensaver should be + * automatically launched when the device is stationary and upright. + * @hide + */ + @Readable + public static final String SCREENSAVER_ACTIVATE_ON_POSTURED = + "screensaver_activate_on_postured"; + + /** * If screensavers are enabled, the default screensaver component. * @hide */ diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig index dfc11dcb5427..d3a230d1335d 100644 --- a/core/java/android/service/dreams/flags.aconfig +++ b/core/java/android/service/dreams/flags.aconfig @@ -77,3 +77,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "allow_dream_when_postured" + namespace: "systemui" + description: "Allow dreaming when device is stationary and upright" + bug: "383208131" +} diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index d53b98c65f9a..3c53506990d1 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -16,7 +16,6 @@ package android.text; -import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; @@ -670,15 +669,11 @@ public abstract class Layout { // High-contrast text mode // Determine if the text is black-on-white or white-on-black, so we know what blendmode will // give the highest contrast and most realistic text color. - // This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h - if (highContrastTextLuminance()) { - var lab = new double[3]; - ColorUtils.colorToLAB(color, lab); - return lab[0] < 50.0; - } else { - int channelSum = Color.red(color) + Color.green(color) + Color.blue(color); - return channelSum < (128 * 3); - } + // LINT.IfChange(hct_darken) + var lab = new double[3]; + ColorUtils.colorToLAB(color, lab); + return lab[0] < 50.0; + // LINT.ThenChange(/libs/hwui/hwui/DrawTextFunctor.h:hct_darken) } private boolean isJustificationRequired(int lineNum) { diff --git a/core/java/android/view/DragEvent.java b/core/java/android/view/DragEvent.java index b65e3ebc3871..77af312eac4a 100644 --- a/core/java/android/view/DragEvent.java +++ b/core/java/android/view/DragEvent.java @@ -157,6 +157,11 @@ public class DragEvent implements Parcelable { private float mOffsetY; /** + * The id of the display where the `mX` and `mY` of this event belongs to. + */ + private int mDisplayId; + + /** * The View#DRAG_FLAG_* flags used to start the current drag, only provided if the target window * has the {@link WindowManager.LayoutParams#PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP} flag * and is only sent with {@link #ACTION_DRAG_STARTED} and {@link #ACTION_DROP}. @@ -297,14 +302,15 @@ public class DragEvent implements Parcelable { private DragEvent() { } - private void init(int action, float x, float y, float offsetX, float offsetY, int flags, - ClipDescription description, ClipData data, SurfaceControl dragSurface, + private void init(int action, float x, float y, float offsetX, float offsetY, int displayId, + int flags, ClipDescription description, ClipData data, SurfaceControl dragSurface, IDragAndDropPermissions dragAndDropPermissions, Object localState, boolean result) { mAction = action; mX = x; mY = y; mOffsetX = offsetX; mOffsetY = offsetY; + mDisplayId = displayId; mFlags = flags; mClipDescription = description; mClipData = data; @@ -315,20 +321,20 @@ public class DragEvent implements Parcelable { } static DragEvent obtain() { - return DragEvent.obtain(0, 0f, 0f, 0f, 0f, 0, null, null, null, null, null, false); + return DragEvent.obtain(0, 0f, 0f, 0f, 0f, 0, 0, null, null, null, null, null, false); } /** @hide */ public static DragEvent obtain(int action, float x, float y, float offsetX, float offsetY, - int flags, Object localState, ClipDescription description, ClipData data, + int displayId, int flags, Object localState, ClipDescription description, ClipData data, SurfaceControl dragSurface, IDragAndDropPermissions dragAndDropPermissions, boolean result) { final DragEvent ev; synchronized (gRecyclerLock) { if (gRecyclerTop == null) { ev = new DragEvent(); - ev.init(action, x, y, offsetX, offsetY, flags, description, data, dragSurface, - dragAndDropPermissions, localState, result); + ev.init(action, x, y, offsetX, offsetY, displayId, flags, description, data, + dragSurface, dragAndDropPermissions, localState, result); return ev; } ev = gRecyclerTop; @@ -339,7 +345,7 @@ public class DragEvent implements Parcelable { ev.mRecycled = false; ev.mNext = null; - ev.init(action, x, y, offsetX, offsetY, flags, description, data, dragSurface, + ev.init(action, x, y, offsetX, offsetY, displayId, flags, description, data, dragSurface, dragAndDropPermissions, localState, result); return ev; @@ -349,8 +355,9 @@ public class DragEvent implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static DragEvent obtain(DragEvent source) { return obtain(source.mAction, source.mX, source.mY, source.mOffsetX, source.mOffsetY, - source.mFlags, source.mLocalState, source.mClipDescription, source.mClipData, - source.mDragSurface, source.mDragAndDropPermissions, source.mDragResult); + source.mDisplayId, source.mFlags, source.mLocalState, source.mClipDescription, + source.mClipData, source.mDragSurface, source.mDragAndDropPermissions, + source.mDragResult); } /** @@ -398,6 +405,11 @@ public class DragEvent implements Parcelable { return mOffsetY; } + /** @hide */ + public int getDisplayId() { + return mDisplayId; + } + /** * Returns the {@link android.content.ClipData} object sent to the system as part of the call * to diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java index 2cc05b0bc4b0..1c36eaf99afa 100644 --- a/core/java/android/view/InputEventReceiver.java +++ b/core/java/android/view/InputEventReceiver.java @@ -177,7 +177,7 @@ public abstract class InputEventReceiver { * drag * if true, the window associated with this input channel has just lost drag */ - public void onDragEvent(boolean isExiting, float x, float y) { + public void onDragEvent(boolean isExiting, float x, float y, int displayId) { } /** diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 8ef0b0eebb8c..36671b901a6b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -10561,13 +10561,13 @@ public final class ViewRootImpl implements ViewParent, } @Override - public void onDragEvent(boolean isExiting, float x, float y) { + public void onDragEvent(boolean isExiting, float x, float y, int displayId) { // force DRAG_EXITED_EVENT if appropriate DragEvent event = DragEvent.obtain( - isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION, - x, y, 0 /* offsetX */, 0 /* offsetY */, 0 /* flags */, null/* localState */, - null/* description */, null /* data */, null /* dragSurface */, - null /* dragAndDropPermissions */, false /* result */); + isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION, x, y, + 0 /* offsetX */, 0 /* offsetY */, displayId, 0 /* flags */, + null/* localState */, null/* description */, null /* data */, + null /* dragSurface */, null /* dragAndDropPermissions */, false /* result */); dispatchDragEvent(event); } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 544f42b9acfa..64277b14098d 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -157,6 +157,9 @@ public final class AccessibilityManager { /** @hide */ public static final int AUTOCLICK_CURSOR_AREA_SIZE_MAX = 100; + /** @hide */ + public static final int AUTOCLICK_CURSOR_AREA_INCREMENT_SIZE = 20; + /** * Activity action: Launch UI to manage which accessibility service or feature is assigned * to the navigation bar Accessibility button. diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index b38feeef290b..f178b0ed2043 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -167,3 +167,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "windowing_sdk" + name: "use_self_sync_transaction_for_layer" + description: "Always use this.getSyncTransaction for assignLayer" + bug: "388127825" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java index 7f7ea8b28546..37500766a4ac 100644 --- a/core/java/com/android/internal/security/VerityUtils.java +++ b/core/java/com/android/internal/security/VerityUtils.java @@ -36,7 +36,6 @@ import com.android.internal.org.bouncycastle.cms.SignerInformationVerifier; import com.android.internal.org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import com.android.internal.org.bouncycastle.operator.OperatorCreationException; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -53,12 +52,6 @@ import java.security.cert.X509Certificate; public abstract class VerityUtils { private static final String TAG = "VerityUtils"; - /** - * File extension of the signature file. For example, foo.apk.fsv_sig is the signature file of - * foo.apk. - */ - public static final String FSVERITY_SIGNATURE_FILE_EXTENSION = ".fsv_sig"; - /** SHA256 hash size. */ private static final int HASH_SIZE_BYTES = 32; @@ -67,16 +60,6 @@ public abstract class VerityUtils { || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2; } - /** Returns true if the given file looks like containing an fs-verity signature. */ - public static boolean isFsveritySignatureFile(File file) { - return file.getName().endsWith(FSVERITY_SIGNATURE_FILE_EXTENSION); - } - - /** Returns the fs-verity signature file path of the given file. */ - public static String getFsveritySignatureFilePath(String filePath) { - return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION; - } - /** Enables fs-verity for the file without signature. */ public static void setUpFsverity(@NonNull String filePath) throws IOException { int errno = enableFsverityNative(filePath); diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java index f0b54937546b..904b73f41e70 100644 --- a/core/java/com/android/internal/widget/NotificationProgressBar.java +++ b/core/java/com/android/internal/widget/NotificationProgressBar.java @@ -42,6 +42,9 @@ import androidx.annotation.ColorInt; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePart; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint; +import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment; import java.util.ArrayList; import java.util.HashMap; @@ -59,6 +62,7 @@ import java.util.TreeSet; public final class NotificationProgressBar extends ProgressBar implements NotificationProgressDrawable.BoundsChangeListener { private static final String TAG = "NotificationProgressBar"; + private static final boolean DEBUG = false; private NotificationProgressDrawable mNotificationProgressDrawable; private final Rect mProgressDrawableBounds = new Rect(); @@ -70,7 +74,7 @@ public final class NotificationProgressBar extends ProgressBar implements // List of drawable parts before segment splitting by process. @Nullable - private List<NotificationProgressDrawable.Part> mProgressDrawableParts = null; + private List<DrawablePart> mProgressDrawableParts = null; @Nullable private Drawable mTracker = null; @@ -109,8 +113,8 @@ public final class NotificationProgressBar extends ProgressBar implements int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - final TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes); saveAttributeDataForStyleable(context, R.styleable.NotificationProgressBar, attrs, a, defStyleAttr, defStyleRes); @@ -140,8 +144,7 @@ public final class NotificationProgressBar extends ProgressBar implements */ @RemotableViewMethod public void setProgressModel(@Nullable Bundle bundle) { - Preconditions.checkArgument(bundle != null, - "Bundle shouldn't be null"); + Preconditions.checkArgument(bundle != null, "Bundle shouldn't be null"); mProgressModel = NotificationProgressModel.fromBundle(bundle); final boolean isIndeterminate = mProgressModel.isIndeterminate(); @@ -381,8 +384,7 @@ public final class NotificationProgressBar extends ProgressBar implements super.drawableStateChanged(); final Drawable tracker = mTracker; - if (tracker != null && tracker.isStateful() - && tracker.setState(getDrawableState())) { + if (tracker != null && tracker.isStateful() && tracker.setState(getDrawableState())) { invalidateDrawable(tracker); } } @@ -417,8 +419,10 @@ public final class NotificationProgressBar extends ProgressBar implements } private void updateDrawableParts() { - Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = " - + mNotificationProgressDrawable + ", mParts = " + mParts); + if (DEBUG) { + Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = " + + mNotificationProgressDrawable + ", mParts = " + mParts); + } if (mNotificationProgressDrawable == null) return; if (mParts == null) return; @@ -426,7 +430,9 @@ public final class NotificationProgressBar extends ProgressBar implements final float width = mNotificationProgressDrawable.getBounds().width(); if (width == 0) { if (mProgressDrawableParts != null) { - Log.d(TAG, "Clearing mProgressDrawableParts"); + if (DEBUG) { + Log.d(TAG, "Clearing mProgressDrawableParts"); + } mProgressDrawableParts.clear(); mNotificationProgressDrawable.setParts(mProgressDrawableParts); } @@ -441,7 +447,7 @@ public final class NotificationProgressBar extends ProgressBar implements mNotificationProgressDrawable.getPointRadius(), mHasTrackerIcon ); - Pair<List<NotificationProgressDrawable.Part>, Float> p = maybeStretchAndRescaleSegments( + Pair<List<DrawablePart>, Float> p = maybeStretchAndRescaleSegments( mParts, mProgressDrawableParts, mNotificationProgressDrawable.getSegmentMinWidth(), @@ -451,7 +457,9 @@ public final class NotificationProgressBar extends ProgressBar implements mProgressModel.isStyledByProgress(), mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap()); - Log.d(TAG, "Updating NotificationProgressDrawable parts"); + if (DEBUG) { + Log.d(TAG, "Updating NotificationProgressDrawable parts"); + } mNotificationProgressDrawable.setParts(p.first); mAdjustedProgressFraction = p.second / width; } @@ -500,11 +508,11 @@ public final class NotificationProgressBar extends ProgressBar implements /** * Updates the tracker drawable bounds. * - * @param w Width of the view, including padding - * @param tracker Drawable used for the tracker + * @param w Width of the view, including padding + * @param tracker Drawable used for the tracker * @param progressFraction Current progress between 0 and 1 - * @param offsetY Vertical offset for centering. If set to - * {@link Integer#MIN_VALUE}, the current offset will be used. + * @param offsetY Vertical offset for centering. If set to + * {@link Integer#MIN_VALUE}, the current offset will be used. */ private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) { int available = w - mPaddingLeft - mPaddingRight; @@ -532,8 +540,8 @@ public final class NotificationProgressBar extends ProgressBar implements if (background != null) { final int bkgOffsetX = mPaddingLeft; final int bkgOffsetY = mPaddingTop; - background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, - right + bkgOffsetX, bottom + bkgOffsetY); + background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, right + bkgOffsetX, + bottom + bkgOffsetY); } // Canvas will be translated, so 0,0 is where we start drawing @@ -668,8 +676,8 @@ public final class NotificationProgressBar extends ProgressBar implements final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap, positionToPointMap); - final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = - splitSegmentsByPoints(startToSegmentMap, sortedPos, progressMax); + final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = splitSegmentsByPoints( + startToSegmentMap, sortedPos, progressMax); return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos, progressMax); @@ -691,8 +699,7 @@ public final class NotificationProgressBar extends ProgressBar implements final ProgressStyle.Segment prevSeg = startToSegmentMap.get(prevSegStart); final ProgressStyle.Segment leftSeg = new ProgressStyle.Segment( - pos - prevSegStart).setColor( - prevSeg.getColor()); + pos - prevSegStart).setColor(prevSeg.getColor()); final ProgressStyle.Segment rightSeg = new ProgressStyle.Segment( prevSegStart + prevSeg.getLength() - pos).setColor(prevSeg.getColor()); @@ -719,8 +726,7 @@ public final class NotificationProgressBar extends ProgressBar implements } if (startToSegmentMap.containsKey(pos)) { final ProgressStyle.Segment seg = startToSegmentMap.get(pos); - parts.add(new Segment( - (float) seg.getLength() / progressMax, seg.getColor())); + parts.add(new Segment((float) seg.getLength() / progressMax, seg.getColor())); } } @@ -787,11 +793,10 @@ public final class NotificationProgressBar extends ProgressBar implements } /** - * Processes the list of {@code Part} and convert to a list of - * {@code NotificationProgressDrawable.Part}. + * Processes the list of {@code Part} and convert to a list of {@code DrawablePart}. */ @VisibleForTesting - public static List<NotificationProgressDrawable.Part> processAndConvertToDrawableParts( + public static List<DrawablePart> processAndConvertToDrawableParts( List<Part> parts, float totalWidth, float segSegGap, @@ -799,7 +804,7 @@ public final class NotificationProgressBar extends ProgressBar implements float pointRadius, boolean hasTrackerIcon ) { - List<NotificationProgressDrawable.Part> drawableParts = new ArrayList<>(); + List<DrawablePart> drawableParts = new ArrayList<>(); // generally, we will start drawing at (x, y) and end at (x+w, y) float x = (float) 0; @@ -820,9 +825,7 @@ public final class NotificationProgressBar extends ProgressBar implements segSegGap, x + segWidth, totalWidth, hasTrackerIcon); final float end = x + segWidth - endOffset; - drawableParts.add( - new NotificationProgressDrawable.Segment(start, end, segment.mColor, - segment.mFaded)); + drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded)); segment.mStart = x; segment.mEnd = x + segWidth; @@ -840,8 +843,7 @@ public final class NotificationProgressBar extends ProgressBar implements if (totalWidth > pointWidth) start = totalWidth - pointWidth; } - drawableParts.add( - new NotificationProgressDrawable.Point(start, end, point.mColor)); + drawableParts.add(new DrawablePoint(start, end, point.mColor)); } } @@ -856,8 +858,8 @@ public final class NotificationProgressBar extends ProgressBar implements } private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, - float segPointGap, - float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) { + float segPointGap, float segSegGap, float endX, float totalWidth, + boolean hasTrackerIcon) { if (nextPart == null) return 0F; if (nextPart instanceof Segment nextSeg) { if (!seg.mFaded && nextSeg.mFaded) { @@ -874,15 +876,14 @@ public final class NotificationProgressBar extends ProgressBar implements } /** - * Processes the list of {@code NotificationProgressBar.Part} data and convert to a pair of: - * - list of {@code NotificationProgressDrawable.Part}. - * - location of progress on the stretched and rescaled progress bar. + * Processes the list of {@code DrawablePart} data and convert to a pair of: + * - list of processed {@code DrawablePart}. + * - location of progress on the stretched and rescaled progress bar. */ @VisibleForTesting - public static Pair<List<NotificationProgressDrawable.Part>, Float> - maybeStretchAndRescaleSegments( + public static Pair<List<DrawablePart>, Float> maybeStretchAndRescaleSegments( List<Part> parts, - List<NotificationProgressDrawable.Part> drawableParts, + List<DrawablePart> drawableParts, float segmentMinWidth, float pointRadius, float progressFraction, @@ -890,14 +891,14 @@ public final class NotificationProgressBar extends ProgressBar implements boolean isStyledByProgress, float progressGap ) { - final List<NotificationProgressDrawable.Segment> drawableSegments = drawableParts + final List<DrawableSegment> drawableSegments = drawableParts .stream() - .filter(NotificationProgressDrawable.Segment.class::isInstance) - .map(NotificationProgressDrawable.Segment.class::cast) + .filter(DrawableSegment.class::isInstance) + .map(DrawableSegment.class::cast) .toList(); float totalExcessWidth = 0; float totalPositiveExcessWidth = 0; - for (NotificationProgressDrawable.Segment drawableSegment : drawableSegments) { + for (DrawableSegment drawableSegment : drawableSegments) { final float excessWidth = drawableSegment.getWidth() - segmentMinWidth; totalExcessWidth += excessWidth; if (excessWidth > 0) totalPositiveExcessWidth += excessWidth; @@ -930,8 +931,8 @@ public final class NotificationProgressBar extends ProgressBar implements final int nParts = drawableParts.size(); float startOffset = 0; for (int iPart = 0; iPart < nParts; iPart++) { - final NotificationProgressDrawable.Part drawablePart = drawableParts.get(iPart); - if (drawablePart instanceof NotificationProgressDrawable.Segment drawableSegment) { + final DrawablePart drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof DrawableSegment drawableSegment) { final float origDrawableSegmentWidth = drawableSegment.getWidth(); float drawableSegmentWidth = segmentMinWidth; @@ -960,7 +961,7 @@ public final class NotificationProgressBar extends ProgressBar implements // Increase startOffset for the subsequent segments. startOffset += widthDiff; - } else if (drawablePart instanceof NotificationProgressDrawable.Point drawablePoint) { + } else if (drawablePart instanceof DrawablePoint drawablePoint) { drawablePoint.setStart(drawablePoint.getStart() + startOffset); drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius); } @@ -975,13 +976,15 @@ public final class NotificationProgressBar extends ProgressBar implements progressGap); } - // Find the location of progress on the stretched and rescaled progress bar. - // If isStyledByProgress is true, also split the segment with the progress value in its range. - private static Pair<List<NotificationProgressDrawable.Part>, Float> - maybeSplitDrawableSegmentsByProgress( + /** + * Find the location of progress on the stretched and rescaled progress bar. + * If isStyledByProgress is true, also split the drawable segment with the progress value in its + * range. Style the drawable parts after process with reduced opacity and segment height. + */ + private static Pair<List<DrawablePart>, Float> maybeSplitDrawableSegmentsByProgress( // Needed to get the original segment start and end positions in pixels. List<Part> parts, - List<NotificationProgressDrawable.Part> drawableParts, + List<DrawablePart> drawableParts, float progressFraction, float totalWidth, boolean isStyledByProgress, @@ -1005,9 +1008,9 @@ public final class NotificationProgressBar extends ProgressBar implements } else if (startFraction < progressFraction && progressFraction < startFraction + segment.mFraction) { iPartSegmentToSplit = iPart; - rescaledProgressX = - segment.mStart + (progressFraction - startFraction) / segment.mFraction - * segment.getWidth(); + rescaledProgressX = segment.mStart + + (progressFraction - startFraction) / segment.mFraction + * segment.getWidth(); break; } startFraction += segment.mFraction; @@ -1015,51 +1018,42 @@ public final class NotificationProgressBar extends ProgressBar implements if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX); - List<NotificationProgressDrawable.Part> splitDrawableParts = new ArrayList<>(); + List<DrawablePart> splitDrawableParts = new ArrayList<>(); boolean styleRemainingParts = false; for (int iPart = 0; iPart < nParts; iPart++) { - final NotificationProgressDrawable.Part drawablePart = drawableParts.get(iPart); - if (drawablePart instanceof NotificationProgressDrawable.Point drawablePoint) { + final DrawablePart drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof DrawablePoint drawablePoint) { final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts); splitDrawableParts.add( - new NotificationProgressDrawable.Point(drawablePoint.getStart(), - drawablePoint.getEnd(), color)); + new DrawablePoint(drawablePoint.getStart(), drawablePoint.getEnd(), color)); } if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true; - if (drawablePart instanceof NotificationProgressDrawable.Segment drawableSegment) { + if (drawablePart instanceof DrawableSegment drawableSegment) { if (iPart == iPartSegmentToSplit) { if (rescaledProgressX <= drawableSegment.getStart()) { styleRemainingParts = true; final int color = maybeGetFadedColor(drawableSegment.getColor(), true); - splitDrawableParts.add( - new NotificationProgressDrawable.Segment(drawableSegment.getStart(), - drawableSegment.getEnd(), - color, true)); + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), color, true)); } else if (drawableSegment.getStart() < rescaledProgressX && rescaledProgressX < drawableSegment.getEnd()) { - splitDrawableParts.add( - new NotificationProgressDrawable.Segment(drawableSegment.getStart(), - rescaledProgressX - progressGap, - drawableSegment.getColor())); + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + rescaledProgressX - progressGap, drawableSegment.getColor())); final int color = maybeGetFadedColor(drawableSegment.getColor(), true); splitDrawableParts.add( - new NotificationProgressDrawable.Segment(rescaledProgressX, - drawableSegment.getEnd(), color, true)); + new DrawableSegment(rescaledProgressX, drawableSegment.getEnd(), + color, true)); styleRemainingParts = true; } else { - splitDrawableParts.add( - new NotificationProgressDrawable.Segment(drawableSegment.getStart(), - drawableSegment.getEnd(), - drawableSegment.getColor())); + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), drawableSegment.getColor())); styleRemainingParts = true; } } else { final int color = maybeGetFadedColor(drawableSegment.getColor(), styleRemainingParts); - splitDrawableParts.add( - new NotificationProgressDrawable.Segment(drawableSegment.getStart(), - drawableSegment.getEnd(), - color, styleRemainingParts)); + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), color, styleRemainingParts)); } } } @@ -1073,22 +1067,21 @@ public final class NotificationProgressBar extends ProgressBar implements */ // TODO: b/372908709 - maybe this should be made private? Only test the final // NotificationDrawable.Parts. - // TODO: b/372908709 - rename to BarPart, BarSegment, BarPoint. This avoids naming ambiguity - // with the types in NotificationProgressDrawable. public interface Part { } /** * A segment is a part of the progress bar with non-zero length. For example, it can * represent a portion in a navigation journey with certain traffic condition. - * */ public static final class Segment implements Part { private final float mFraction; - @ColorInt private final int mColor; - /** Whether the segment is faded or not. + @ColorInt + private final int mColor; + /** + * Whether the segment is faded or not. * <p> - * <pre> + * <pre> * When mFaded is set to true, a combination of the following is done to the segment: * 1. The drawing color is mColor with opacity updated to 40%. * 2. The gap between faded and non-faded segments is: @@ -1150,7 +1143,8 @@ public final class NotificationProgressBar extends ProgressBar implements * ride-share journey. */ public static final class Point implements Part { - @ColorInt private final int mColor; + @ColorInt + private final int mColor; public Point(@ColorInt int color) { mColor = color; diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java index ef0a5d5cdec2..4ece81c24edc 100644 --- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java +++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java @@ -51,8 +51,8 @@ import java.util.Objects; * segments, which have non-zero length varying drawing width, and points, which have zero length * and fixed size for drawing. * - * @see Segment - * @see Point + * @see DrawableSegment + * @see DrawablePoint */ public final class NotificationProgressDrawable extends Drawable { private static final String TAG = "NotifProgressDrawable"; @@ -63,7 +63,7 @@ public final class NotificationProgressDrawable extends Drawable { private State mState; private boolean mMutated; - private final ArrayList<Part> mParts = new ArrayList<>(); + private final ArrayList<DrawablePart> mParts = new ArrayList<>(); private final RectF mSegRectF = new RectF(); private final RectF mPointRectF = new RectF(); @@ -111,7 +111,7 @@ public final class NotificationProgressDrawable extends Drawable { /** * Set the segments and points that constitute the drawable. */ - public void setParts(List<Part> parts) { + public void setParts(List<DrawablePart> parts) { mParts.clear(); mParts.addAll(parts); @@ -121,7 +121,7 @@ public final class NotificationProgressDrawable extends Drawable { /** * Set the segments and points that constitute the drawable. */ - public void setParts(@NonNull Part... parts) { + public void setParts(@NonNull DrawablePart... parts) { setParts(Arrays.asList(parts)); } @@ -133,10 +133,10 @@ public final class NotificationProgressDrawable extends Drawable { final int numParts = mParts.size(); for (int iPart = 0; iPart < numParts; iPart++) { - final Part part = mParts.get(iPart); + final DrawablePart part = mParts.get(iPart); final float start = left + part.mStart; final float end = left + part.mEnd; - if (part instanceof Segment segment) { + if (part instanceof DrawableSegment segment) { // No space left to draw the segment if (start > end) continue; @@ -148,7 +148,7 @@ public final class NotificationProgressDrawable extends Drawable { mSegRectF.set(start, centerY - radiusY, end, centerY + radiusY); canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint); - } else if (part instanceof Point point) { + } else if (part instanceof DrawablePoint point) { // TODO: b/367804171 - actually use a vector asset for the default point // rather than drawing it as a box? mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); @@ -413,10 +413,11 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * A part of the progress bar, which is either a {@link Segment} with non-zero length and - * varying drawing width, or a {@link Point} with zero length and fixed size for drawing. + * A part of the progress drawable, which is either a {@link DrawableSegment} with non-zero + * length and varying drawing width, or a {@link DrawablePoint} with zero length and fixed size + * for drawing. */ - public abstract static class Part { + public abstract static class DrawablePart { // TODO: b/372908709 - maybe rename start/end to left/right, to be consistent with the // bounds rect. /** Start position for drawing (in pixels) */ @@ -426,7 +427,7 @@ public final class NotificationProgressDrawable extends Drawable { /** Drawing color. */ @ColorInt protected final int mColor; - protected Part(float start, float end, @ColorInt int color) { + protected DrawablePart(float start, float end, @ColorInt int color) { mStart = start; mEnd = end; mColor = color; @@ -464,7 +465,7 @@ public final class NotificationProgressDrawable extends Drawable { if (other == null || getClass() != other.getClass()) return false; - Part that = (Part) other; + DrawablePart that = (DrawablePart) other; if (Float.compare(this.mStart, that.mStart) != 0) return false; if (Float.compare(this.mEnd, that.mEnd) != 0) return false; return this.mColor == that.mColor; @@ -484,7 +485,7 @@ public final class NotificationProgressDrawable extends Drawable { * the Points and gaps neighboring the segment. * </p> */ - public static final class Segment extends Part { + public static final class DrawableSegment extends DrawablePart { /** * Whether the segment is faded or not. * <p> @@ -493,11 +494,11 @@ public final class NotificationProgressDrawable extends Drawable { */ private final boolean mFaded; - public Segment(float start, float end, int color) { + public DrawableSegment(float start, float end, int color) { this(start, end, color, false); } - public Segment(float start, float end, int color, boolean faded) { + public DrawableSegment(float start, float end, int color, boolean faded) { super(start, end, color); mFaded = faded; } @@ -513,7 +514,7 @@ public final class NotificationProgressDrawable extends Drawable { public boolean equals(@Nullable Object other) { if (!super.equals(other)) return false; - Segment that = (Segment) other; + DrawableSegment that = (DrawableSegment) other; return this.mFaded == that.mFaded; } @@ -528,8 +529,8 @@ public final class NotificationProgressDrawable extends Drawable { * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop * ride-share journey. */ - public static final class Point extends Part { - public Point(float start, float end, int color) { + public static final class DrawablePoint extends DrawablePart { + public DrawablePoint(float start, float end, int color) { super(start, end, color); } diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp index 3c2dccd451d4..9ef17e82c38e 100644 --- a/core/jni/android_os_Debug.cpp +++ b/core/jni/android_os_Debug.cpp @@ -729,6 +729,17 @@ static jlong android_os_Debug_getGpuPrivateMemoryKb(JNIEnv* env, jobject clazz) return gpuPrivateMem / 1024; } +static jlong android_os_Debug_getKernelCmaUsageKb(JNIEnv* env, jobject clazz) { + jlong totalKernelCmaUsageKb = -1; + uint64_t size; + + if (meminfo::ReadKernelCmaUsageKb(&size)) { + totalKernelCmaUsageKb = size; + } + + return totalKernelCmaUsageKb; +} + static jlong android_os_Debug_getDmabufMappedSizeKb(JNIEnv* env, jobject clazz) { jlong dmabufPss = 0; std::vector<dmabufinfo::DmaBuffer> dmabufs; @@ -836,6 +847,7 @@ static const JNINativeMethod gMethods[] = { {"getGpuTotalUsageKb", "()J", (void*)android_os_Debug_getGpuTotalUsageKb}, {"isVmapStack", "()Z", (void*)android_os_Debug_isVmapStack}, {"logAllocatorStats", "()Z", (void*)android_os_Debug_logAllocatorStats}, + {"getKernelCmaUsageKb", "()J", (void*)android_os_Debug_getKernelCmaUsageKb}, }; int register_android_os_Debug(JNIEnv *env) diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp index 3a1e8835c8db..6272fb1947c1 100644 --- a/core/jni/android_view_InputEventReceiver.cpp +++ b/core/jni/android_view_InputEventReceiver.cpp @@ -441,7 +441,8 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, } env->CallVoidMethod(receiverObj.get(), gInputEventReceiverClassInfo.onDragEvent, jboolean(dragEvent->isExiting()), dragEvent->getX(), - dragEvent->getY()); + dragEvent->getY(), + static_cast<jint>(dragEvent->getDisplayId().val())); finishInputEvent(seq, /*handled=*/true); continue; } @@ -643,7 +644,7 @@ int register_android_view_InputEventReceiver(JNIEnv* env) { GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onPointerCaptureEvent", "(Z)V"); gInputEventReceiverClassInfo.onDragEvent = - GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onDragEvent", "(ZFF)V"); + GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onDragEvent", "(ZFFI)V"); gInputEventReceiverClassInfo.onTouchModeChanged = GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onTouchModeChanged", "(Z)V"); gInputEventReceiverClassInfo.onBatchedInputEventPending = diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 4f7ba9388a1d..5d0b340ac839 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -580,6 +580,7 @@ message SecureSettingsProto { optional SettingProto activate_on_dock = 3 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto activate_on_sleep = 4 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto default_component = 5 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto activate_on_postured = 6 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Screensaver screensaver = 47; diff --git a/core/res/res/values-round-watch/dimens.xml b/core/res/res/values-round-watch/dimens.xml index f288b41fb556..59ee554798bc 100644 --- a/core/res/res/values-round-watch/dimens.xml +++ b/core/res/res/values-round-watch/dimens.xml @@ -26,6 +26,6 @@ <item name="input_extract_action_button_height" type="dimen">32dp</item> <item name="input_extract_action_icon_padding" type="dimen">5dp</item> - <item name="global_actions_vertical_padding_percentage" type="fraction">20.8%</item> + <item name="global_actions_vertical_padding_percentage" type="fraction">21.8%</item> <item name="global_actions_horizontal_padding_percentage" type="fraction">5.2%</item> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index ce9a0c636d62..e14cffd72b0c 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2766,6 +2766,9 @@ <bool name="config_dreamsActivatedOnDockByDefault">true</bool> <!-- If supported and enabled, are dreams activated when asleep and charging? (by default) --> <bool name="config_dreamsActivatedOnSleepByDefault">false</bool> + <!-- If supported and enabled, are dreams enabled while device is stationary and upright? + (by default) --> + <bool name="config_dreamsActivatedOnPosturedByDefault">false</bool> <!-- ComponentName of the default dream (Settings.Secure.DEFAULT_SCREENSAVER_COMPONENT) --> <string name="config_dreamsDefaultComponent" translatable="false">com.android.deskclock/com.android.deskclock.Screensaver</string> <!-- ComponentNames of the dreams that we should hide --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index debc5e9a0dce..fa4c21de682e 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6646,6 +6646,8 @@ ul.</string> <string name="satellite_notification_title">Auto connected to satellite</string> <!-- Notification summary when satellite service is auto connected. [CHAR LIMIT=NONE] --> <string name="satellite_notification_summary">You can send and receive messages without a mobile or Wi-Fi network</string> + <!-- Notification summary when satellite service connected with data service supported. [CHAR LIMIT=NONE] --> + <string name="satellite_notification_summary_with_data">You can send and receive messages and use limited data by satellite</string> <!-- Notification title when satellite service can be manually enabled. --> <string name="satellite_notification_manual_title">Use satellite messaging?</string> <!-- Notification summary when satellite service can be manually enabled. [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 6c014e93d4cc..68008e57094d 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2330,6 +2330,7 @@ <java-symbol type="bool" name="config_dreamsEnabledOnBattery" /> <java-symbol type="bool" name="config_dreamsActivatedOnDockByDefault" /> <java-symbol type="bool" name="config_dreamsActivatedOnSleepByDefault" /> + <java-symbol type="bool" name="config_dreamsActivatedOnPosturedByDefault" /> <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenPowered" /> <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenNotPowered" /> <java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" /> @@ -5651,6 +5652,7 @@ <!-- System notification for satellite service --> <java-symbol type="string" name="satellite_notification_title" /> <java-symbol type="string" name="satellite_notification_summary" /> + <java-symbol type="string" name="satellite_notification_summary_with_data" /> <java-symbol type="string" name="satellite_notification_manual_title" /> <java-symbol type="string" name="satellite_notification_manual_summary" /> <java-symbol type="string" name="satellite_notification_open_message" /> diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 06cd44e6544d..9b3a6cba5f23 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -111,7 +111,7 @@ <shortcode country="cz" premium="90\\d{5}|90\\d{3}" free="116\\d{3}" /> <!-- Germany: 4-5 digits plus 1232xxx (premium codes from http://www.vodafone.de/infofaxe/537.pdf and http://premiumdienste.eplus.de/pdf/kodex.pdf), plus EU. To keep the premium regex from being too large, it only includes payment processors that have been used by SMS malware, with the regular pattern matching the other premium short codes. --> - <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}|81214|81215|47529|70296|83782|3011|73240|72438" /> + <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}|81214|81215|47529|70296|83782|3011|73240|72438|70997" /> <!-- Denmark: see http://iprs.webspacecommerce.com/Denmark-Premium-Rate-Numbers --> <shortcode country="dk" pattern="\\d{4,5}" premium="1\\d{3}" free="116\\d{3}|4665" /> diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java index f105ec305eab..5df2c1279eb8 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java @@ -27,6 +27,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.widget.NotificationProgressBar.Part; import com.android.internal.widget.NotificationProgressBar.Point; import com.android.internal.widget.NotificationProgressBar.Segment; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePart; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint; +import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment; import org.junit.Test; import org.junit.runner.RunWith; @@ -120,28 +123,25 @@ public class NotificationProgressBarTest { float pointRadius = 6; boolean hasTrackerIcon = true; - List<NotificationProgressDrawable.Part> drawableParts = - NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon - ); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); - List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 300, Color.RED))); + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, Color.RED))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); float segmentMinWidth = 16; boolean isStyledByProgress = true; - Pair<List<NotificationProgressDrawable.Part>, Float> p = - NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, - segmentMinWidth, pointRadius, (float) progress / progressMax, 300, - isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedRed = 0x66FF0000; expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 300, fadedRed, true))); + List.of(new DrawableSegment(0, 300, fadedRed, true))); assertThat(p.second).isEqualTo(0); assertThat(p.first).isEqualTo(expectedDrawableParts); @@ -168,23 +168,20 @@ public class NotificationProgressBarTest { float pointRadius = 6; boolean hasTrackerIcon = true; - List<NotificationProgressDrawable.Part> drawableParts = - NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon - ); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); - List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 300, Color.RED))); + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, Color.RED))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); float segmentMinWidth = 16; boolean isStyledByProgress = true; - Pair<List<NotificationProgressDrawable.Part>, Float> p = - NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, - segmentMinWidth, pointRadius, (float) progress / progressMax, 300, - isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); assertThat(p.second).isEqualTo(300); assertThat(p.first).isEqualTo(expectedDrawableParts); @@ -237,12 +234,11 @@ public class NotificationProgressBarTest { int progress = 60; int progressMax = 100; - List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( - segments, points, progress, progressMax); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expectedParts = new ArrayList<>(List.of( - new Segment(0.50f, Color.RED), - new Segment(0.50f, Color.GREEN))); + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN))); assertThat(parts).isEqualTo(expectedParts); @@ -252,30 +248,26 @@ public class NotificationProgressBarTest { float pointRadius = 6; boolean hasTrackerIcon = true; - List<NotificationProgressDrawable.Part> drawableParts = - NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon - ); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); - List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), - new NotificationProgressDrawable.Segment(150, 300, Color.GREEN))); + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 300, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); float segmentMinWidth = 16; boolean isStyledByProgress = true; - Pair<List<NotificationProgressDrawable.Part>, Float> p = - NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, - segmentMinWidth, pointRadius, (float) progress / progressMax, 300, - isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; - expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), - new NotificationProgressDrawable.Segment(150, 180, Color.GREEN), - new NotificationProgressDrawable.Segment(180, 300, fadedGreen, true))); + expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 180, Color.GREEN), + new DrawableSegment(180, 300, fadedGreen, true))); assertThat(p.second).isEqualTo(180); assertThat(p.first).isEqualTo(expectedDrawableParts); @@ -292,9 +284,8 @@ public class NotificationProgressBarTest { List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, progressMax); - List<Part> expectedParts = new ArrayList<>(List.of( - new Segment(0.50f, Color.RED), - new Segment(0.50f, Color.GREEN))); + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN))); assertThat(parts).isEqualTo(expectedParts); @@ -304,30 +295,26 @@ public class NotificationProgressBarTest { float pointRadius = 6; boolean hasTrackerIcon = false; - List<NotificationProgressDrawable.Part> drawableParts = - NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon - ); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); - List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), - new NotificationProgressDrawable.Segment(150, 300, Color.GREEN))); + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 300, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); float segmentMinWidth = 16; boolean isStyledByProgress = true; - Pair<List<NotificationProgressDrawable.Part>, Float> p = - NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, - segmentMinWidth, pointRadius, (float) progress / progressMax, 300, - isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; - expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), - new NotificationProgressDrawable.Segment(150, 176, Color.GREEN), - new NotificationProgressDrawable.Segment(180, 300, fadedGreen, true))); + expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 176, Color.GREEN), + new DrawableSegment(180, 300, fadedGreen, true))); assertThat(p.second).isEqualTo(180); assertThat(p.first).isEqualTo(expectedDrawableParts); @@ -348,16 +335,12 @@ public class NotificationProgressBarTest { List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, progressMax); - List<Part> expectedParts = new ArrayList<>(List.of( - new Segment(0.15f, Color.BLUE), - new Point(Color.RED), - new Segment(0.10f, Color.BLUE), - new Point(Color.BLUE), - new Segment(0.35f, Color.BLUE), - new Point(Color.BLUE), - new Segment(0.15f, Color.BLUE), - new Point(Color.YELLOW), - new Segment(0.25f, Color.BLUE))); + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.BLUE), new Point(Color.RED), + new Segment(0.10f, Color.BLUE), new Point(Color.BLUE), + new Segment(0.35f, Color.BLUE), new Point(Color.BLUE), + new Segment(0.15f, Color.BLUE), new Point(Color.YELLOW), + new Segment(0.25f, Color.BLUE))); assertThat(parts).isEqualTo(expectedParts); @@ -367,47 +350,42 @@ public class NotificationProgressBarTest { float pointRadius = 6; boolean hasTrackerIcon = true; - List<NotificationProgressDrawable.Part> drawableParts = - NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon - ); - - List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 35, Color.BLUE), - new NotificationProgressDrawable.Point(39, 51, Color.RED), - new NotificationProgressDrawable.Segment(55, 65, Color.BLUE), - new NotificationProgressDrawable.Point(69, 81, Color.BLUE), - new NotificationProgressDrawable.Segment(85, 170, Color.BLUE), - new NotificationProgressDrawable.Point(174, 186, Color.BLUE), - new NotificationProgressDrawable.Segment(190, 215, Color.BLUE), - new NotificationProgressDrawable.Point(219, 231, Color.YELLOW), - new NotificationProgressDrawable.Segment(235, 300, Color.BLUE))); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.BLUE), + new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.BLUE), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 170, Color.BLUE), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 215, Color.BLUE), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); float segmentMinWidth = 16; boolean isStyledByProgress = true; - Pair<List<NotificationProgressDrawable.Part>, Float> p = - NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, - segmentMinWidth, pointRadius, (float) progress / progressMax, 300, - isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedBlue = 0x660000FF; int fadedYellow = 0x66FFFF00; expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 34.219177F, Color.BLUE), - new NotificationProgressDrawable.Point(38.219177F, 50.219177F, Color.RED), - new NotificationProgressDrawable.Segment(54.219177F, 70.21918F, Color.BLUE), - new NotificationProgressDrawable.Point(74.21918F, 86.21918F, Color.BLUE), - new NotificationProgressDrawable.Segment(90.21918F, 172.38356F, Color.BLUE), - new NotificationProgressDrawable.Point(176.38356F, 188.38356F, Color.BLUE), - new NotificationProgressDrawable.Segment(192.38356F, 217.0137F, fadedBlue, - true), - new NotificationProgressDrawable.Point(221.0137F, 233.0137F, fadedYellow), - new NotificationProgressDrawable.Segment(237.0137F, 300F, fadedBlue, - true))); + List.of(new DrawableSegment(0, 34.219177F, Color.BLUE), + new DrawablePoint(38.219177F, 50.219177F, Color.RED), + new DrawableSegment(54.219177F, 70.21918F, Color.BLUE), + new DrawablePoint(74.21918F, 86.21918F, Color.BLUE), + new DrawableSegment(90.21918F, 172.38356F, Color.BLUE), + new DrawablePoint(176.38356F, 188.38356F, Color.BLUE), + new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true), + new DrawablePoint(221.0137F, 233.0137F, fadedYellow), + new DrawableSegment(237.0137F, 300F, fadedBlue, true))); assertThat(p.second).isEqualTo(182.38356F); assertThat(p.first).isEqualTo(expectedDrawableParts); @@ -429,17 +407,12 @@ public class NotificationProgressBarTest { List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, progressMax); - List<Part> expectedParts = new ArrayList<>(List.of( - new Segment(0.15f, Color.RED), - new Point(Color.RED), - new Segment(0.10f, Color.RED), - new Point(Color.BLUE), - new Segment(0.25f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Point(Color.BLUE), - new Segment(0.15f, Color.GREEN), - new Point(Color.YELLOW), - new Segment(0.25f, Color.GREEN))); + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.RED), new Point(Color.RED), + new Segment(0.10f, Color.RED), new Point(Color.BLUE), + new Segment(0.25f, Color.RED), new Segment(0.10f, Color.GREEN), + new Point(Color.BLUE), new Segment(0.15f, Color.GREEN), + new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); assertThat(parts).isEqualTo(expectedParts); @@ -448,51 +421,43 @@ public class NotificationProgressBarTest { float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; - List<NotificationProgressDrawable.Part> drawableParts = - NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon - ); - - List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 35, Color.RED), - new NotificationProgressDrawable.Point(39, 51, Color.RED), - new NotificationProgressDrawable.Segment(55, 65, Color.RED), - new NotificationProgressDrawable.Point(69, 81, Color.BLUE), - new NotificationProgressDrawable.Segment(85, 146, Color.RED), - new NotificationProgressDrawable.Segment(150, 170, Color.GREEN), - new NotificationProgressDrawable.Point(174, 186, Color.BLUE), - new NotificationProgressDrawable.Segment(190, 215, Color.GREEN), - new NotificationProgressDrawable.Point(219, 231, Color.YELLOW), - new NotificationProgressDrawable.Segment(235, 300, Color.GREEN))); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 170, Color.GREEN), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 215, Color.GREEN), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); float segmentMinWidth = 16; boolean isStyledByProgress = true; - Pair<List<NotificationProgressDrawable.Part>, Float> p = - NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, - segmentMinWidth, pointRadius, (float) progress / progressMax, 300, - isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; int fadedYellow = 0x66FFFF00; expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 34.095238F, Color.RED), - new NotificationProgressDrawable.Point(38.095238F, 50.095238F, Color.RED), - new NotificationProgressDrawable.Segment(54.095238F, 70.09524F, Color.RED), - new NotificationProgressDrawable.Point(74.09524F, 86.09524F, Color.BLUE), - new NotificationProgressDrawable.Segment(90.09524F, 148.9524F, Color.RED), - new NotificationProgressDrawable.Segment(152.95238F, 172.7619F, - Color.GREEN), - new NotificationProgressDrawable.Point(176.7619F, 188.7619F, Color.BLUE), - new NotificationProgressDrawable.Segment(192.7619F, 217.33333F, - fadedGreen, true), - new NotificationProgressDrawable.Point(221.33333F, 233.33333F, - fadedYellow), - new NotificationProgressDrawable.Segment(237.33333F, 299.99997F, - fadedGreen, true))); + List.of(new DrawableSegment(0, 34.095238F, Color.RED), + new DrawablePoint(38.095238F, 50.095238F, Color.RED), + new DrawableSegment(54.095238F, 70.09524F, Color.RED), + new DrawablePoint(74.09524F, 86.09524F, Color.BLUE), + new DrawableSegment(90.09524F, 148.9524F, Color.RED), + new DrawableSegment(152.95238F, 172.7619F, Color.GREEN), + new DrawablePoint(176.7619F, 188.7619F, Color.BLUE), + new DrawableSegment(192.7619F, 217.33333F, fadedGreen, true), + new DrawablePoint(221.33333F, 233.33333F, fadedYellow), + new DrawableSegment(237.33333F, 299.99997F, fadedGreen, true))); assertThat(p.second).isEqualTo(182.7619F); assertThat(p.first).isEqualTo(expectedDrawableParts); @@ -513,15 +478,11 @@ public class NotificationProgressBarTest { List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, progressMax); - List<Part> expectedParts = new ArrayList<>(List.of( - new Segment(0.15f, Color.RED), - new Point(Color.RED), - new Segment(0.10f, Color.RED), - new Point(Color.BLUE), - new Segment(0.25f, Color.RED), - new Segment(0.25f, Color.GREEN), - new Point(Color.YELLOW), - new Segment(0.25f, Color.GREEN))); + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.RED), new Point(Color.RED), + new Segment(0.10f, Color.RED), new Point(Color.BLUE), + new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN), + new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); assertThat(parts).isEqualTo(expectedParts); @@ -531,42 +492,36 @@ public class NotificationProgressBarTest { float pointRadius = 6; boolean hasTrackerIcon = true; - List<NotificationProgressDrawable.Part> drawableParts = - NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon - ); - - List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 35, Color.RED), - new NotificationProgressDrawable.Point(39, 51, Color.RED), - new NotificationProgressDrawable.Segment(55, 65, Color.RED), - new NotificationProgressDrawable.Point(69, 81, Color.BLUE), - new NotificationProgressDrawable.Segment(85, 146, Color.RED), - new NotificationProgressDrawable.Segment(150, 215, Color.GREEN), - new NotificationProgressDrawable.Point(219, 231, Color.YELLOW), - new NotificationProgressDrawable.Segment(235, 300, Color.GREEN))); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 215, Color.GREEN), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.GREEN))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); float segmentMinWidth = 16; boolean isStyledByProgress = false; - Pair<List<NotificationProgressDrawable.Part>, Float> p = - NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, - segmentMinWidth, pointRadius, (float) progress / progressMax, 300, - isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Segment(0, 34.296295F, Color.RED), - new NotificationProgressDrawable.Point(38.296295F, 50.296295F, Color.RED), - new NotificationProgressDrawable.Segment(54.296295F, 70.296295F, Color.RED), - new NotificationProgressDrawable.Point(74.296295F, 86.296295F, Color.BLUE), - new NotificationProgressDrawable.Segment(90.296295F, 149.62962F, Color.RED), - new NotificationProgressDrawable.Segment(153.62962F, 216.8148F, - Color.GREEN), - new NotificationProgressDrawable.Point(220.81482F, 232.81482F, - Color.YELLOW), - new NotificationProgressDrawable.Segment(236.81482F, 300, Color.GREEN))); + List.of(new DrawableSegment(0, 34.296295F, Color.RED), + new DrawablePoint(38.296295F, 50.296295F, Color.RED), + new DrawableSegment(54.296295F, 70.296295F, Color.RED), + new DrawablePoint(74.296295F, 86.296295F, Color.BLUE), + new DrawableSegment(90.296295F, 149.62962F, Color.RED), + new DrawableSegment(153.62962F, 216.8148F, Color.GREEN), + new DrawablePoint(220.81482F, 232.81482F, Color.YELLOW), + new DrawableSegment(236.81482F, 300, Color.GREEN))); assertThat(p.second).isEqualTo(182.9037F); assertThat(p.first).isEqualTo(expectedDrawableParts); @@ -586,15 +541,13 @@ public class NotificationProgressBarTest { int progress = 1000; int progressMax = 1000; - List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( - segments, points, progress, progressMax); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expectedParts = new ArrayList<>(List.of( - new Point(Color.BLUE), - new Segment(0.1f, Color.BLUE), - new Segment(0.2f, Color.BLUE), - new Segment(0.3f, Color.BLUE), - new Segment(0.4f, Color.BLUE))); + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); assertThat(parts).isEqualTo(expectedParts); @@ -603,34 +556,30 @@ public class NotificationProgressBarTest { float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; - List<NotificationProgressDrawable.Part> drawableParts = - NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon - ); - - List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), - new NotificationProgressDrawable.Segment(16, 16, Color.BLUE), - new NotificationProgressDrawable.Segment(20, 56, Color.BLUE), - new NotificationProgressDrawable.Segment(60, 116, Color.BLUE), - new NotificationProgressDrawable.Segment(120, 200, Color.BLUE))); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 16, Color.BLUE), + new DrawableSegment(20, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); float segmentMinWidth = 16; boolean isStyledByProgress = true; - Pair<List<NotificationProgressDrawable.Part>, Float> p = - NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, - segmentMinWidth, pointRadius, (float) progress / progressMax, 200, - isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), - new NotificationProgressDrawable.Segment(16, 32, Color.BLUE), - new NotificationProgressDrawable.Segment(36, 69.41936F, Color.BLUE), - new NotificationProgressDrawable.Segment(73.41936F, 124.25807F, Color.BLUE), - new NotificationProgressDrawable.Segment(128.25807F, 200, Color.BLUE))); + expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 32, Color.BLUE), + new DrawableSegment(36, 69.41936F, Color.BLUE), + new DrawableSegment(73.41936F, 124.25807F, Color.BLUE), + new DrawableSegment(128.25807F, 200, Color.BLUE))); assertThat(p.second).isEqualTo(200); assertThat(p.first).isEqualTo(expectedDrawableParts); @@ -650,15 +599,13 @@ public class NotificationProgressBarTest { int progress = 1000; int progressMax = 1000; - List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( - segments, points, progress, progressMax); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expectedParts = new ArrayList<>(List.of( - new Point(Color.BLUE), - new Segment(0.1f, Color.BLUE), - new Segment(0.2f, Color.BLUE), - new Segment(0.3f, Color.BLUE), - new Segment(0.4f, Color.BLUE))); + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); assertThat(parts).isEqualTo(expectedParts); @@ -667,35 +614,30 @@ public class NotificationProgressBarTest { float segPointGap = 4; float pointRadius = 6; boolean hasTrackerIcon = true; - List<NotificationProgressDrawable.Part> drawableParts = - NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon - ); - - List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), - new NotificationProgressDrawable.Segment(16, 16, Color.BLUE), - new NotificationProgressDrawable.Segment(20, 56, Color.BLUE), - new NotificationProgressDrawable.Segment(60, 116, Color.BLUE), - new NotificationProgressDrawable.Segment(120, 200, Color.BLUE))); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 16, Color.BLUE), + new DrawableSegment(20, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); float segmentMinWidth = 10; boolean isStyledByProgress = true; - Pair<List<NotificationProgressDrawable.Part>, Float> p = - NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, - segmentMinWidth, pointRadius, (float) progress / progressMax, 200, - isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), - new NotificationProgressDrawable.Segment(16, 26, Color.BLUE), - new NotificationProgressDrawable.Segment(30, 64.169014F, Color.BLUE), - new NotificationProgressDrawable.Segment(68.169014F, 120.92958F, - Color.BLUE), - new NotificationProgressDrawable.Segment(124.92958F, 200, Color.BLUE))); + expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 26, Color.BLUE), + new DrawableSegment(30, 64.169014F, Color.BLUE), + new DrawableSegment(68.169014F, 120.92958F, Color.BLUE), + new DrawableSegment(124.92958F, 200, Color.BLUE))); assertThat(p.second).isEqualTo(200); assertThat(p.first).isEqualTo(expectedDrawableParts); @@ -713,15 +655,13 @@ public class NotificationProgressBarTest { int progress = 1000; int progressMax = 1000; - List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( - segments, points, progress, progressMax); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expectedParts = new ArrayList<>(List.of( - new Point(Color.BLUE), - new Segment(0.2f, Color.BLUE), - new Segment(0.1f, Color.BLUE), - new Segment(0.3f, Color.BLUE), - new Segment(0.4f, Color.BLUE))); + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.2f, Color.BLUE), + new Segment(0.1f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); assertThat(parts).isEqualTo(expectedParts); @@ -731,27 +671,24 @@ public class NotificationProgressBarTest { float pointRadius = 6; boolean hasTrackerIcon = true; - List<NotificationProgressDrawable.Part> drawableParts = - NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, - segSegGap, segPointGap, pointRadius, hasTrackerIcon - ); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); - List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( - List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), - new NotificationProgressDrawable.Segment(16, 36, Color.BLUE), - new NotificationProgressDrawable.Segment(40, 56, Color.BLUE), - new NotificationProgressDrawable.Segment(60, 116, Color.BLUE), - new NotificationProgressDrawable.Segment(120, 200, Color.BLUE))); + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 36, Color.BLUE), + new DrawableSegment(40, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); assertThat(drawableParts).isEqualTo(expectedDrawableParts); float segmentMinWidth = 10; boolean isStyledByProgress = true; - Pair<List<NotificationProgressDrawable.Part>, Float> p = - NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, - segmentMinWidth, pointRadius, (float) progress / progressMax, 200, - isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); assertThat(p.second).isEqualTo(200); assertThat(p.first).isEqualTo(expectedDrawableParts); diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 3378cc11d565..b332cf0d751f 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -19,6 +19,7 @@ package android.graphics; import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; +import static com.android.text.flags.Flags.FLAG_TYPEFACE_REDESIGN_READONLY; import static com.android.text.flags.Flags.FLAG_VERTICAL_TEXT_LAYOUT; import android.annotation.ColorInt; @@ -57,6 +58,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Objects; @@ -96,6 +98,7 @@ public class Paint { private LocaleList mLocales; private String mFontFeatureSettings; private String mFontVariationSettings; + private String mFontVariationOverride; private float mShadowLayerRadius; private float mShadowLayerDx; @@ -2140,6 +2143,12 @@ public class Paint { * @see #getFontVariationSettings() * @see FontVariationAxis */ + // Add following API description once the setFontVariationOverride becomes public. + // This method generates new variation instance of the {@link Typeface} instance and set it to + // this object. Therefore, subsequent {@link #setTypeface(Typeface)} call will clear the font + // variation settings. Also, creating variation instance of the Typeface requires non trivial + // amount of time and memories, therefore consider using + // {@link #setFontVariationOverride(String, int)} for better performance. public boolean setFontVariationSettings(String fontVariationSettings) { final String settings = TextUtils.nullIfEmpty(fontVariationSettings); if (settings == mFontVariationSettings @@ -2174,6 +2183,68 @@ public class Paint { } /** + * Sets TrueType or OpenType font variation settings for overriding. + * + * The settings string is constructed from multiple pairs of axis tag and style values. The axis + * tag must contain four ASCII characters and must be wrapped with single quotes (U+0027) or + * double quotes (U+0022). Axis strings that are longer or shorter than four characters, or + * contain characters outside of U+0020..U+007E are invalid. + * + * If invalid font variation settings is provided, this method does nothing and returning false + * with printing error message to the logcat. + * + * Different from {@link #setFontVariationSettings(String)}, this overrides the font variation + * settings which is already assigned to the font instance. For example, if the underlying font + * is configured as {@code 'wght' 500, 'ital' 1}, and if the override is specified as + * {@code 'wght' 700, `wdth` 150}, then the effective font variation setting is + * {@code `wght' 700, 'ital' 1, 'wdth' 150}. The `wght` value is updated by override, 'ital' + * value is preserved because no overrides, and `wdth` value is added by override. + * + * @param fontVariationOverride font variation settings. You can pass null or empty string as + * no variation settings. + * + * @return true if the provided font variation settings is valid. Otherwise returns false. + * + * @see #getFontVariationSettings() + * @see #setFontVariationSettings(String) + * @see #getFontVariationOverride() + * @see FontVariationAxis + */ + @FlaggedApi(FLAG_TYPEFACE_REDESIGN_READONLY) + public boolean setFontVariationOverride(@Nullable String fontVariationOverride) { + if (Objects.equals(fontVariationOverride, mFontVariationOverride)) { + return true; + } + + List<FontVariationAxis> axes; + try { + axes = FontVariationAxis.fromFontVariationSettingsForList(fontVariationOverride); + } catch (IllegalArgumentException e) { + Log.i(TAG, "failed to parse font variation settings.", e); + return false; + } + long builderPtr = nCreateFontVariationBuilder(axes.size()); + for (int i = 0; i < axes.size(); ++i) { + FontVariationAxis axis = axes.get(i); + nAddFontVariationToBuilder( + builderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue()); + } + nSetFontVariationOverride(mNativePaint, builderPtr); + mFontVariationOverride = fontVariationOverride; + return true; + } + + /** + * Gets the current font variation override value. + * + * @return a current font variation override value. + */ + @FlaggedApi(FLAG_TYPEFACE_REDESIGN_READONLY) + public @Nullable String getFontVariationOverride() { + return mFontVariationOverride; + } + + /** * Get the current value of start hyphen edit. * * The default value is 0 which is equivalent to {@link #START_HYPHEN_EDIT_NO_EDIT}. diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java index 740ccb53a691..13f1a72469c2 100644 --- a/keystore/java/android/security/keystore/KeyStoreManager.java +++ b/keystore/java/android/security/keystore/KeyStoreManager.java @@ -312,9 +312,11 @@ public final class KeyStoreManager { * When passed into getSupplementaryAttestationInfo, getSupplementaryAttestationInfo returns the * DER-encoded structure corresponding to the `Modules` schema described in the KeyMint HAL's * KeyCreationResult.aidl. The SHA-256 hash of this encoded structure is what's included with - * the tag in attestations. + * the tag in attestations. To ensure the returned encoded structure is the one attested to, + * clients should verify its SHA-256 hash matches the one in the attestation. Note that the + * returned structure can vary between boots. */ - // TODO(b/369375199): Replace with Tag.MODULE_HASH when flagging is removed. + // TODO(b/380020528): Replace with Tag.MODULE_HASH when KeyMint V4 is frozen. public static final int MODULE_HASH = TagType.BYTES | 724; /** diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index b2ac640a468d..4f1cd9780f8b 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -26,6 +26,7 @@ <uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" /> <uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION" /> <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" /> + <uses-permission android:name="android.permission.MANAGE_DISPLAYS" /> <application> <activity diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt index d15fbed409b8..23498de72481 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt @@ -32,9 +32,7 @@ enum class DesktopModeTransitionSource : Parcelable { /** Transitions with source unknown. */ UNKNOWN; - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int = 0 override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeString(name) @@ -44,9 +42,8 @@ enum class DesktopModeTransitionSource : Parcelable { @JvmField val CREATOR = object : Parcelable.Creator<DesktopModeTransitionSource> { - override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource { - return parcel.readString()?.let { valueOf(it) } ?: UNKNOWN - } + override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource = + parcel.readString()?.let { valueOf(it) } ?: UNKNOWN override fun newArray(size: Int) = arrayOfNulls<DesktopModeTransitionSource>(size) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java index 8cd7b0f48003..82ef00e46e8c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java @@ -16,6 +16,7 @@ package com.android.wm.shell.appzoomout; +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import android.app.ActivityManager; @@ -100,6 +101,7 @@ public class AppZoomOutController implements RemoteCallable<AppZoomOutController mDisplayController.addDisplayWindowListener(mDisplaysChangedListener); mDisplayController.addDisplayChangingController(this); + updateDisplayLayout(mContext.getDisplayId()); mDisplayAreaOrganizer.registerOrganizer(); } @@ -135,7 +137,9 @@ public class AppZoomOutController implements RemoteCallable<AppZoomOutController public void onDisplayChange(int displayId, int fromRotation, int toRotation, @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) { // TODO: verify if there is synchronization issues. - mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation); + if (toRotation != ROTATION_UNDEFINED) { + mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation); + } } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index f532be6b8277..72be066fc7a7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayTopology; import android.os.RemoteException; import android.util.ArraySet; import android.util.Size; @@ -34,6 +35,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; +import com.android.window.flags.Flags; import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; @@ -54,6 +56,7 @@ public class DisplayController { private final ShellExecutor mMainExecutor; private final Context mContext; private final IWindowManager mWmService; + private final DisplayManager mDisplayManager; private final DisplayChangeController mChangeController; private final IDisplayWindowListener mDisplayContainerListener; @@ -61,10 +64,11 @@ public class DisplayController { private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, DisplayManager displayManager) { mMainExecutor = mainExecutor; mContext = context; mWmService = wmService; + mDisplayManager = displayManager; // TODO: Inject this instead mChangeController = new DisplayChangeController(mWmService, shellInit, mainExecutor); mDisplayContainerListener = new DisplayWindowListenerImpl(); @@ -74,7 +78,7 @@ public class DisplayController { } /** - * Initializes the window listener. + * Initializes the window listener and the topology listener. */ public void onInit() { try { @@ -82,6 +86,12 @@ public class DisplayController { for (int i = 0; i < displayIds.length; i++) { onDisplayAdded(displayIds[i]); } + + if (Flags.enableConnectedDisplaysWindowDrag()) { + mDisplayManager.registerTopologyListener(mMainExecutor, + this::onDisplayTopologyChanged); + onDisplayTopologyChanged(mDisplayManager.getDisplayTopology()); + } } catch (RemoteException e) { throw new RuntimeException("Unable to register display controller"); } @@ -91,8 +101,7 @@ public class DisplayController { * Gets a display by id from DisplayManager. */ public Display getDisplay(int displayId) { - final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); - return displayManager.getDisplay(displayId); + return mDisplayManager.getDisplay(displayId); } /** @@ -221,6 +230,14 @@ public class DisplayController { } } + private void onDisplayTopologyChanged(DisplayTopology topology) { + // TODO(b/381472611): Call DisplayTopology#getCoordinates and update values in + // DisplayLayout when DM code is ready. + for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { + mDisplayChangedListeners.get(i).onTopologyChanged(); + } + } + private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { synchronized (mDisplays) { final DisplayRecord dr = mDisplays.get(displayId); @@ -408,5 +425,10 @@ public class DisplayController { */ default void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) {} + + /** + * Called when the display topology has changed. + */ + default void onTopologyChanged() {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java index b6a1686bd087..4973a6f16409 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -31,7 +31,9 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.graphics.Insets; +import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.RectF; import android.os.SystemProperties; import android.provider.Settings; import android.util.DisplayMetrics; @@ -71,9 +73,12 @@ public class DisplayLayout { public static final int NAV_BAR_RIGHT = 1 << 1; public static final int NAV_BAR_BOTTOM = 1 << 2; + private static final String TAG = "DisplayLayout"; + private int mUiMode; private int mWidth; private int mHeight; + private RectF mGlobalBoundsDp; private DisplayCutout mCutout; private int mRotation; private int mDensityDpi; @@ -109,6 +114,7 @@ public class DisplayLayout { return mUiMode == other.mUiMode && mWidth == other.mWidth && mHeight == other.mHeight + && Objects.equals(mGlobalBoundsDp, other.mGlobalBoundsDp) && Objects.equals(mCutout, other.mCutout) && mRotation == other.mRotation && mDensityDpi == other.mDensityDpi @@ -127,8 +133,8 @@ public class DisplayLayout { @Override public int hashCode() { - return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi, - mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar, + return Objects.hash(mUiMode, mWidth, mHeight, mGlobalBoundsDp, mCutout, mRotation, + mDensityDpi, mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar, mNavBarFrameHeight, mTaskbarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving, mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState); } @@ -170,6 +176,7 @@ public class DisplayLayout { mUiMode = dl.mUiMode; mWidth = dl.mWidth; mHeight = dl.mHeight; + mGlobalBoundsDp = dl.mGlobalBoundsDp; mCutout = dl.mCutout; mRotation = dl.mRotation; mDensityDpi = dl.mDensityDpi; @@ -193,6 +200,7 @@ public class DisplayLayout { mRotation = info.rotation; mCutout = info.displayCutout; mDensityDpi = info.logicalDensityDpi; + mGlobalBoundsDp = new RectF(0, 0, pxToDp(mWidth), pxToDp(mHeight)); mHasNavigationBar = hasNavigationBar; mHasStatusBar = hasStatusBar; mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean( @@ -255,6 +263,11 @@ public class DisplayLayout { recalcInsets(res); } + /** Update the global bounds of this layout, in DP. */ + public void setGlobalBoundsDp(RectF bounds) { + mGlobalBoundsDp = bounds; + } + /** Get this layout's non-decor insets. */ public Rect nonDecorInsets() { return mNonDecorInsets; @@ -265,16 +278,21 @@ public class DisplayLayout { return mStableInsets; } - /** Get this layout's width. */ + /** Get this layout's width in pixels. */ public int width() { return mWidth; } - /** Get this layout's height. */ + /** Get this layout's height in pixels. */ public int height() { return mHeight; } + /** Get this layout's global bounds in the multi-display coordinate system in DP. */ + public RectF globalBoundsDp() { + return mGlobalBoundsDp; + } + /** Get this layout's display rotation. */ public int rotation() { return mRotation; @@ -486,4 +504,48 @@ public class DisplayLayout { ? R.dimen.navigation_bar_frame_height_landscape : R.dimen.navigation_bar_frame_height); } + + /** + * Converts a pixel value to a density-independent pixel (dp) value. + * + * @param px The pixel value to convert. + * @return The equivalent value in DP units. + */ + public float pxToDp(Number px) { + return px.floatValue() * DisplayMetrics.DENSITY_DEFAULT / mDensityDpi; + } + + /** + * Converts a density-independent pixel (dp) value to a pixel value. + * + * @param dp The DP value to convert. + * @return The equivalent value in pixel units. + */ + public float dpToPx(Number dp) { + return dp.floatValue() * mDensityDpi / DisplayMetrics.DENSITY_DEFAULT; + } + + /** + * Converts local pixel coordinates on this layout to global DP coordinates. + * + * @param xPx The x-coordinate in pixels, relative to the layout's origin. + * @param yPx The y-coordinate in pixels, relative to the layout's origin. + * @return A PointF object representing the coordinates in global DP units. + */ + public PointF localPxToGlobalDp(Number xPx, Number yPx) { + return new PointF(mGlobalBoundsDp.left + pxToDp(xPx), + mGlobalBoundsDp.top + pxToDp(yPx)); + } + + /** + * Converts global DP coordinates to local pixel coordinates on this layout. + * + * @param xDp The x-coordinate in global DP units. + * @param yDp The y-coordinate in global DP units. + * @return A PointF object representing the coordinates in local pixel units on this layout. + */ + public PointF globalDpToLocalPx(Number xDp, Number yDp) { + return new PointF(dpToPx(xDp.floatValue() - mGlobalBoundsDp.left), + dpToPx(yDp.floatValue() - mGlobalBoundsDp.top)); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index ab3c33ec7e43..cbbe8a2b5613 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -25,6 +25,7 @@ import android.annotation.NonNull; import android.app.ActivityTaskManager; import android.content.Context; import android.content.pm.PackageManager; +import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.SystemProperties; import android.provider.Settings; @@ -175,8 +176,9 @@ public abstract class WMShellBaseModule { static DisplayController provideDisplayController(Context context, IWindowManager wmService, ShellInit shellInit, - @ShellMainThread ShellExecutor mainExecutor) { - return new DisplayController(context, wmService, shellInit, mainExecutor); + @ShellMainThread ShellExecutor mainExecutor, + DisplayManager displayManager) { + return new DisplayController(context, wmService, shellInit, mainExecutor, displayManager); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt index ca02c72c174e..f6fd9679922a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt @@ -93,9 +93,8 @@ class DesktopModeDragAndDropTransitionHandler(private val transitions: Transitio return matchingChanges.first() } - private fun isValidTaskChange(change: TransitionInfo.Change): Boolean { - return change.taskInfo != null && change.taskInfo?.taskId != -1 - } + private fun isValidTaskChange(change: TransitionInfo.Change): Boolean = + change.taskInfo != null && change.taskInfo?.taskId != -1 override fun handleRequest( transition: IBinder, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index e975b586c1ee..c09504ee3725 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -434,18 +434,14 @@ class DesktopModeLoggerTransitionObserver( visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo) } - private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo { - return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change") - } + private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo = + this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change") - private fun TaskInfo.isFreeformWindow(): Boolean { - return this.windowingMode == WINDOWING_MODE_FREEFORM - } + private fun TaskInfo.isFreeformWindow(): Boolean = this.windowingMode == WINDOWING_MODE_FREEFORM - private fun TransitionInfo.isExitToRecentsTransition(): Boolean { - return this.type == WindowManager.TRANSIT_TO_FRONT && + private fun TransitionInfo.isExitToRecentsTransition(): Boolean = + this.type == WindowManager.TRANSIT_TO_FRONT && this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS - } companion object { @VisibleForTesting const val VISIBLE_TASKS_COUNTER_NAME = "desktop_mode_visible_tasks" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index dba8c9367654..cdfa14bbc4e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -24,8 +24,8 @@ import java.io.PrintWriter class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) : ShellCommandHandler.ShellCommandActionHandler { - override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean { - return when (args[0]) { + override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean = + when (args[0]) { "moveToDesktop" -> { if (!runMoveToDesktop(args, pw)) { pw.println("Task not found. Please enter a valid taskId.") @@ -47,7 +47,6 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl false } } - } private fun runMoveToDesktop(args: Array<String>, pw: PrintWriter): Boolean { if (args.size < 2) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt index 848d80ff4f0b..f29301d92292 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt @@ -41,49 +41,35 @@ sealed class DesktopTaskPosition { return Point(x, y.toInt()) } - override fun next(): DesktopTaskPosition { - return BottomRight - } + override fun next(): DesktopTaskPosition = BottomRight } data object BottomRight : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.right - window.width(), frame.bottom - window.height()) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.right - window.width(), frame.bottom - window.height()) - override fun next(): DesktopTaskPosition { - return TopLeft - } + override fun next(): DesktopTaskPosition = TopLeft } data object TopLeft : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.left, frame.top) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.left, frame.top) - override fun next(): DesktopTaskPosition { - return BottomLeft - } + override fun next(): DesktopTaskPosition = BottomLeft } data object BottomLeft : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.left, frame.bottom - window.height()) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.left, frame.bottom - window.height()) - override fun next(): DesktopTaskPosition { - return TopRight - } + override fun next(): DesktopTaskPosition = TopRight } data object TopRight : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.right - window.width(), frame.top) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.right - window.width(), frame.top) - override fun next(): DesktopTaskPosition { - return Center - } + override fun next(): DesktopTaskPosition = Center } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 6013648c9806..73d15270c811 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -1470,13 +1470,9 @@ class DesktopTasksController( } } - override fun getContext(): Context { - return context - } + override fun getContext(): Context = context - override fun getRemoteCallExecutor(): ShellExecutor { - return mainExecutor - } + override fun getRemoteCallExecutor(): ShellExecutor = mainExecutor override fun startAnimation( transition: IBinder, @@ -1662,11 +1658,10 @@ class DesktopTasksController( DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() && isTopActivityExemptFromDesktopWindowing(context, task) - private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean { - return ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() && + private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean = + ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() && TransitionUtil.isClosingType(request.type) && request.triggerTask != null - } /** Open an existing instance of an app. */ fun openInstance(callingTask: RunningTaskInfo, requestedTaskId: Int) { @@ -2185,11 +2180,10 @@ class DesktopTasksController( getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) } } - private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? { - return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo -> + private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? = + shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo -> taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM } - } /** * Requests a task be transitioned from desktop to split select. Applies needed windowing @@ -2237,14 +2231,10 @@ class DesktopTasksController( } } - private fun getDefaultDensityDpi(): Int { - return context.resources.displayMetrics.densityDpi - } + private fun getDefaultDensityDpi(): Int = context.resources.displayMetrics.densityDpi /** Creates a new instance of the external interface to pass to another process. */ - private fun createExternalInterface(): ExternalInterfaceBinder { - return IDesktopModeImpl(this) - } + private fun createExternalInterface(): ExternalInterfaceBinder = IDesktopModeImpl(this) /** Get connection interface between sysui and shell */ fun asDesktopMode(): DesktopMode { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index c2dd4d28305b..e4a28e9efe60 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -138,11 +138,10 @@ class DesktopTasksLimiter( ) } - private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? { - return info.changes.find { change -> + private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? = + info.changes.find { change -> change.taskInfo?.taskId == taskId && change.mode == TRANSIT_TO_BACK } - } override fun onTransitionMerged(merged: IBinder, playing: IBinder) { if (activeTransitionTokensAndTasks.remove(merged) != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 1380a9ca164f..91f10dc4faf5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -765,9 +765,8 @@ sealed class DragToDesktopTransitionHandler( transitionState = null } - private fun isSplitTask(taskId: Int): Boolean { - return splitScreenController.isTaskInSplitScreen(taskId) - } + private fun isSplitTask(taskId: Int): Boolean = + splitScreenController.isTaskInSplitScreen(taskId) private fun getOtherSplitTask(taskId: Int): Int? { val splitPos = splitScreenController.getSplitPosition(taskId) @@ -781,9 +780,8 @@ sealed class DragToDesktopTransitionHandler( return splitScreenController.getTaskInfo(otherTaskPos)?.taskId } - protected fun requireTransitionState(): TransitionState { - return transitionState ?: error("Expected non-null transition state") - } + protected fun requireTransitionState(): TransitionState = + transitionState ?: error("Expected non-null transition state") /** * Represents the layering (Z order) that will be given to any window based on its type during diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index a47e9370b58d..5e84019b14f5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -156,13 +156,11 @@ class ToggleResizeDesktopTaskTransitionHandler( return matchingChanges.first() } - private fun isWallpaper(change: TransitionInfo.Change): Boolean { - return (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0 - } + private fun isWallpaper(change: TransitionInfo.Change): Boolean = + (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0 - private fun isValidTaskChange(change: TransitionInfo.Change): Boolean { - return change.taskInfo != null && change.taskInfo?.taskId != -1 - } + private fun isValidTaskChange(change: TransitionInfo.Change): Boolean = + change.taskInfo != null && change.taskInfo?.taskId != -1 companion object { private const val RESIZE_DURATION_MS = 300L diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java index a62dd1c83520..4df9ae4b2ee3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java @@ -107,8 +107,11 @@ public class DragUtils { /** * Returns a list of the mime types provided in the clip description. */ - public static String getMimeTypesConcatenated(ClipDescription description) { + public static String getMimeTypesConcatenated(@Nullable ClipDescription description) { String mimeTypes = ""; + if (description == null) { + return mimeTypes; + } for (int i = 0; i < description.getMimeTypeCount(); i++) { if (i > 0) { mimeTypes += ", "; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java index 1e5e153fdfe1..d3de0f7c09b4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.hardware.display.DisplayManager; import android.view.IWindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -50,12 +51,14 @@ public class DisplayControllerTests extends ShellTestCase { private @Mock IWindowManager mWM; private @Mock ShellInit mShellInit; private @Mock ShellExecutor mMainExecutor; + private @Mock DisplayManager mDisplayManager; private DisplayController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new DisplayController(mContext, mWM, mShellInit, mMainExecutor); + mController = new DisplayController( + mContext, mWM, mShellInit, mMainExecutor, mDisplayManager); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java index d467b399ebbb..b0a455d1bcf8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java @@ -33,7 +33,9 @@ import static org.mockito.Mockito.when; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; +import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.RectF; import android.view.DisplayCutout; import android.view.DisplayInfo; @@ -58,6 +60,7 @@ import org.mockito.quality.Strictness; @SmallTest public class DisplayLayoutTest extends ShellTestCase { private MockitoSession mMockitoSession; + private static final float DELTA = 0.1f; // Constant for assertion delta @Before public void setup() { @@ -130,6 +133,39 @@ public class DisplayLayoutTest extends ShellTestCase { assertEquals(new Rect(40, 0, 60, 0), dl.nonDecorInsets()); } + @Test + public void testDpPxConversion() { + int px = 100; + float dp = 53.33f; + int xPx = 100; + int yPx = 200; + float xDp = 164.33f; + float yDp = 328.66f; + + Resources res = createResources(40, 50, false); + DisplayInfo info = createDisplayInfo(1000, 1500, 0, ROTATION_0); + DisplayLayout dl = new DisplayLayout(info, res, false, false); + dl.setGlobalBoundsDp(new RectF(111f, 222f, 300f, 400f)); + + // Test pxToDp + float resultDp = dl.pxToDp(px); + assertEquals(dp, resultDp, DELTA); + + // Test dpToPx + float resultPx = dl.dpToPx(dp); + assertEquals(px, resultPx, DELTA); + + // Test localPxToGlobalDp + PointF resultGlobalDp = dl.localPxToGlobalDp(xPx, yPx); + assertEquals(xDp, resultGlobalDp.x, DELTA); + assertEquals(yDp, resultGlobalDp.y, DELTA); + + // Test globalDpToLocalPx + PointF resultLocalPx = dl.globalDpToLocalPx(xDp, yDp); + assertEquals(xPx, resultLocalPx.x, DELTA); + assertEquals(yPx, resultLocalPx.y, DELTA); + } + private Resources createResources(int navLand, int navPort, boolean navMoves) { Configuration cfg = new Configuration(); cfg.uiMode = UI_MODE_TYPE_NORMAL; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index 9b24c1c06cec..eb6f1d75e6ca 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -501,13 +501,12 @@ class DesktopModeEventLoggerTest : ShellTestCase() { } } - private fun createTaskInfo(): RunningTaskInfo { - return TestRunningTaskInfoBuilder() + private fun createTaskInfo(): RunningTaskInfo = + TestRunningTaskInfoBuilder() .setTaskId(TASK_ID) .setUid(TASK_UID) .setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT)) .build() - } private fun verifyNoLogging() { verify( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 4bb743079861..95ed8b4d4511 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -353,8 +353,8 @@ class DesktopTasksControllerTest : ShellTestCase() { taskRepository = userRepositories.current } - private fun createController(): DesktopTasksController { - return DesktopTasksController( + private fun createController() = + DesktopTasksController( context, shellInit, shellCommandHandler, @@ -388,7 +388,6 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopWallpaperActivityTokenProvider, Optional.of(bubbleController), ) - } @After fun tearDown() { @@ -4958,13 +4957,12 @@ class DesktopTasksControllerTest : ShellTestCase() { return task } - private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo { + private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo = // active = false marks the task as non-visible; PiP window doesn't count as visible tasks - return setUpFreeformTask(active = false).apply { + setUpFreeformTask(active = false).apply { pictureInPictureParams = PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build() } - } private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createHomeTask(displayId) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index 96ed214e7f88..622cb4cad363 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -458,8 +458,8 @@ class DesktopTasksTransitionObserverTest { } } - private fun createCloseTransition(task: RunningTaskInfo?): TransitionInfo { - return TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply { + private fun createCloseTransition(task: RunningTaskInfo?) = + TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply { addChange( Change(mock(), mock()).apply { mode = TRANSIT_CLOSE @@ -469,10 +469,9 @@ class DesktopTasksTransitionObserverTest { } ) } - } - private fun createToBackTransition(task: RunningTaskInfo?): TransitionInfo { - return TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply { + private fun createToBackTransition(task: RunningTaskInfo?) = + TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply { addChange( Change(mock(), mock()).apply { mode = TRANSIT_TO_BACK @@ -482,7 +481,6 @@ class DesktopTasksTransitionObserverTest { } ) } - } private fun createToFrontTransition(task: RunningTaskInfo?): TransitionInfo { return TransitionInfo(TRANSIT_TO_FRONT, 0 /* flags */).apply { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 341df0299a97..bf9cf00050dc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -676,8 +676,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } } - private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo { - return TransitionInfo(type, /* flags= */ 0).apply { + private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo) = + TransitionInfo(type, /* flags= */ 0).apply { addChange( // Home. TransitionInfo.Change(mock(), homeTaskLeash).apply { parent = null @@ -700,7 +700,6 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } ) } - } private fun systemPropertiesKey(name: String) = "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name" diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index 32bb8bbdbbe3..1b1a5a909220 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -152,17 +152,16 @@ public class DragAndDropControllerTest extends ShellTestCase { } @Test - public void testOnDragStarted_withNoClipData() { + public void testOnDragStarted_withNoClipDataOrDescription() { final View dragLayout = mock(View.class); final Display display = mock(Display.class); doReturn(display).when(dragLayout).getDisplay(); doReturn(DEFAULT_DISPLAY).when(display).getDisplayId(); - final ClipData clipData = createAppClipData(MIMETYPE_APPLICATION_SHORTCUT); final DragEvent event = mock(DragEvent.class); doReturn(ACTION_DRAG_STARTED).when(event).getAction(); doReturn(null).when(event).getClipData(); - doReturn(clipData.getDescription()).when(event).getClipDescription(); + doReturn(null).when(event).getClipDescription(); // Ensure there's a target so that onDrag will execute mController.addDisplayDropTarget(0, mContext, mock(WindowManager.class), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt index d410151b4602..5389c94bc15d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt @@ -43,7 +43,7 @@ import org.mockito.kotlin.verify */ @SmallTest @RunWith(AndroidJUnit4::class) -class UnhandledDragControllerTest : ShellTestCase() { +class GlobalDragListenerTest : ShellTestCase() { private val mIWindowManager = mock<IWindowManager>() private val mMainExecutor = mock<ShellExecutor>() @@ -74,7 +74,7 @@ class UnhandledDragControllerTest : ShellTestCase() { @Test fun onUnhandledDrop_noListener_expectNotifyUnhandled() { // Simulate an unhandled drop - val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null, + val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, 0, null, null, null, null, null, false) val wmCallback = mock<IUnhandledDragCallback>() mController.onUnhandledDrop(dropEvent, wmCallback) @@ -98,7 +98,7 @@ class UnhandledDragControllerTest : ShellTestCase() { // Simulate an unhandled drop val dragSurface = mock<SurfaceControl>() - val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null, + val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, 0, null, null, null, dragSurface, null, false) val wmCallback = mock<IUnhandledDragCallback>() mController.onUnhandledDrop(dropEvent, wmCallback) diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 76ad2acccf89..5e71d3360f39 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -34,13 +34,6 @@ flag { } flag { - name: "high_contrast_text_luminance" - namespace: "accessibility" - description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic" - bug: "186567103" -} - -flag { name: "high_contrast_text_small_text_rect" namespace: "accessibility" description: "Draw a solid rectangle background behind text instead of a stroke outline" diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index e13e136550ca..e05c3d695463 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -34,9 +34,6 @@ namespace flags = com::android::graphics::hwui::flags; #else namespace flags { -constexpr bool high_contrast_text_luminance() { - return false; -} constexpr bool high_contrast_text_small_text_rect() { return false; } @@ -114,15 +111,10 @@ public: if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) { // high contrast draw path int color = paint.getColor(); - bool darken; - // This equation should match the one in core/java/android/text/Layout.java - if (flags::high_contrast_text_luminance()) { - uirenderer::Lab lab = uirenderer::sRGBToLab(color); - darken = lab.L <= 50; - } else { - int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color); - darken = channelSum < (128 * 3); - } + // LINT.IfChange(hct_darken) + uirenderer::Lab lab = uirenderer::sRGBToLab(color); + bool darken = lab.L <= 50; + // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken) // outline gDrawTextBlobMode = DrawTextBlobMode::HctOutline; diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index 6571d92aeafa..a67aea466c1c 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -729,7 +729,7 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { VkSemaphore semaphore; VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore); ALOGE_IF(VK_SUCCESS != err, - "VulkanManager::makeSwapSemaphore(): Failed to create semaphore"); + "VulkanManager::finishFrame(): Failed to create semaphore"); if (err == VK_SUCCESS) { sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore); @@ -777,7 +777,7 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { int fenceFd = -1; VkResult err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd); - ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd"); + ALOGE_IF(VK_SUCCESS != err, "VulkanManager::finishFrame(): Failed to get semaphore Fd"); drawResult.presentFence.reset(fenceFd); } else { ALOGE("VulkanManager::finishFrame(): Semaphore submission failed"); diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 54a87ad7fd39..2a740f85aa72 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -32,6 +32,7 @@ import android.media.BluetoothProfileConnectionInfo; import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; +import android.media.IAudioManagerNative; import android.media.IAudioModeDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; @@ -83,6 +84,7 @@ interface IAudioService { // When a method's argument list is changed, BpAudioManager's corresponding serialization code // (if any) in frameworks/native/services/audiomanager/IAudioManager.cpp must be updated. + IAudioManagerNative getNativeInterface(); int trackPlayer(in PlayerBase.PlayerIdCard pic); oneway void playerAttributes(in int piid, in AudioAttributes attr); diff --git a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm index b384a2418ff2..b0308b5dccd4 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm @@ -120,78 +120,90 @@ key EQUALS { key Q { label: 'Q' - base, capslock+shift: 'q' + base: 'q' shift, capslock: 'Q' + shift+capslock: 'q' } key W { label: 'W' - base, capslock+shift: 'w' + base: 'w' shift, capslock: 'W' + shift+capslock: 'w' } key E { label: 'E' - base, capslock+shift: 'e' + base: 'e' shift, capslock: 'E' + shift+capslock: 'e' ralt: '\u20ac' } key R { label: 'R' - base, capslock+shift: 'r' + base: 'r' shift, capslock: 'R' + shift+capslock: 'r' } key T { label: 'T' - base, capslock+shift: 't' + base: 't' shift, capslock: 'T' + shift+capslock: 't' } key Y { label: 'Y' - base, capslock+shift: 'y' + base: 'y' shift, capslock: 'Y' + shift+capslock: 'y' } key U { label: 'U' - base, capslock+shift: 'u' + base: 'u' shift, capslock: 'U' + shift+capslock: 'u' } key I { label: 'I' - base, capslock+shift: 'i' + base: 'i' shift, capslock: 'I' + shift+capslock: 'i' } key O { label: 'O' - base, capslock+shift: 'o' + base: 'o' shift, capslock: 'O' + shift+capslock: 'o' } key P { label: 'P' - base, capslock+shift: 'p' + base: 'p' shift, capslock: 'P' + shift+capslock: 'p' ralt: '\u00a7' } key LEFT_BRACKET { label: '\u0102' - base, capslock+shift: '\u0103' + base: '\u0103' shift, capslock: '\u0102' + shift+capslock: '\u0103' ralt: '[' ralt+shift: '{' } key RIGHT_BRACKET { label: '\u00ce' - base, capslock+shift: '\u00ee' + base: '\u00ee' shift, capslock: '\u00ce' + shift+capslock: '\u00ee' ralt: ']' ralt+shift: '}' } @@ -200,21 +212,24 @@ key RIGHT_BRACKET { key A { label: 'A' - base, capslock+shift: 'a' + base: 'a' shift, capslock: 'A' + shift+capslock: 'a' } key S { label: 'S' - base, capslock+shift: 's' + base: 's' shift, capslock: 'S' + shift+capslock: 's' ralt: '\u00df' } key D { label: 'D' - base, capslock+shift: 'd' + base: 'd' shift, capslock: 'D' + shift+capslock: 'd' ralt: '\u0111' ralt+shift, ralt+capslock: '\u0110' ralt+shift+capslock: '\u0111' @@ -222,38 +237,44 @@ key D { key F { label: 'F' - base, capslock+shift: 'f' + base: 'f' shift, capslock: 'F' + shift+capslock: 'f' } key G { label: 'G' - base, capslock+shift: 'g' + base: 'g' shift, capslock: 'G' + shift+capslock: 'g' } key H { label: 'H' - base, capslock+shift: 'h' + base: 'h' shift, capslock: 'H' + shift+capslock: 'h' } key J { label: 'J' - base, capslock+shift: 'j' + base: 'j' shift, capslock: 'J' + shift+capslock: 'j' } key K { label: 'K' - base, capslock+shift: 'k' + base: 'k' shift, capslock: 'K' + shift+capslock: 'k' } key L { label: 'L' - base, capslock+shift: 'l' + base: 'l' shift, capslock: 'L' + shift+capslock: 'l' ralt: '\u0142' ralt+shift, ralt+capslock: '\u0141' ralt+shift+capslock: '\u0142' @@ -261,24 +282,27 @@ key L { key SEMICOLON { label: '\u0218' - base, capslock+shift: '\u0219' + base: '\u0219' shift, capslock: '\u0218' + shift+capslock: '\u0219' ralt: ';' ralt+shift: ':' } key APOSTROPHE { label: '\u021a' - base, capslock+shift: '\u021b' + base: '\u021b' shift, capslock: '\u021a' + shift+capslock: '\u021b' ralt: '\'' ralt+shift: '\u0022' } key BACKSLASH { label: '\u00c2' - base, capslock+shift: '\u00e2' + base: '\u00e2' shift, capslock: '\u00c2' + shift+capslock: '\u00e2' ralt: '\\' ralt+shift: '|' } @@ -293,45 +317,52 @@ key PLUS { key Z { label: 'Z' - base, capslock+shift: 'z' + base: 'z' shift, capslock: 'Z' + shift+capslock: 'z' } key X { label: 'X' - base, capslock+shift: 'x' + base: 'x' shift, capslock: 'X' + shift+capslock: 'x' } key C { label: 'C' - base, capslock+shift: 'c' + base: 'c' shift, capslock: 'C' + shift+capslock: 'c' ralt: '\u00a9' } key V { label: 'V' - base, capslock+shift: 'v' + base: 'v' shift, capslock: 'V' + shift+capslock: 'v' } key B { label: 'B' - base, capslock+shift: 'b' + base: 'b' shift, capslock: 'B' + shift+capslock: 'b' } key N { label: 'N' - base, capslock+shift: 'n' + base: 'n' shift, capslock: 'N' + shift+capslock: 'n' } key M { label: 'M' - base, capslock+shift: 'm' + base: 'm' shift, capslock: 'M' + shift+capslock: 'm' } key COMMA { diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm index 6fa54f9d052f..9df78c9af923 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm @@ -104,149 +104,173 @@ key EQUALS { key Q { label: '\u0409' - base, capslock+shift: '\u0459' + base: '\u0459' shift, capslock: '\u0409' + shift+capslock: '\u0459' } key W { label: '\u040a' - base, capslock+shift: '\u045a' + base: '\u045a' shift, capslock: '\u040a' + shift+capslock: '\u045a' } key E { label: '\u0415' - base, capslock+shift: '\u0435' + base: '\u0435' shift, capslock: '\u0415' + shift+capslock: '\u0435' ralt: '\u20ac' } key R { label: '\u0420' - base, capslock+shift: '\u0440' + base: '\u0440' shift, capslock: '\u0420' + shift+capslock: '\u0440' } key T { label: '\u0422' - base, capslock+shift: '\u0442' + base: '\u0442' shift, capslock: '\u0422' + shift+capslock: '\u0442' } key Y { label: '\u0417' - base, capslock+shift: '\u0437' + base: '\u0437' shift, capslock: '\u0417' + shift+capslock: '\u0437' } key U { label: '\u0423' - base, capslock+shift: '\u0443' + base: '\u0443' shift, capslock: '\u0423' + shift+capslock: '\u0443' } key I { label: '\u0418' - base, capslock+shift: '\u0438' + base: '\u0438' shift, capslock: '\u0418' + shift+capslock: '\u0438' } key O { label: '\u041e' - base, capslock+shift: '\u043e' + base: '\u043e' shift, capslock: '\u041e' + shift+capslock: '\u043e' } key P { label: '\u041f' - base, capslock+shift: '\u043f' + base: '\u043f' shift, capslock: '\u041f' + shift+capslock: '\u043f' } key LEFT_BRACKET { label: '\u0428' - base, capslock+shift: '\u0448' + base: '\u0448' shift, capslock: '\u0428' + shift+capslock: '\u0448' } key RIGHT_BRACKET { label: '\u0402' - base, capslock+shift: '\u0452' + base: '\u0452' shift, capslock: '\u0402' + shift+capslock: '\u0452' } ### ROW 3 key A { label: '\u0410' - base, capslock+shift: '\u0430' + base: '\u0430' shift, capslock: '\u0410' + shift+capslock: '\u0430' } key S { label: '\u0421' - base, capslock+shift: '\u0441' + base: '\u0441' shift, capslock: '\u0421' + shift+capslock: '\u0441' } key D { label: '\u0414' - base, capslock+shift: '\u0434' + base: '\u0434' shift, capslock: '\u0414' + shift+capslock: '\u0434' } key F { label: '\u0424' - base, capslock+shift: '\u0444' + base: '\u0444' shift, capslock: '\u0424' + shift+capslock: '\u0444' } key G { label: '\u0413' - base, capslock+shift: '\u0433' + base: '\u0433' shift, capslock: '\u0413' + shift+capslock: '\u0433' } key H { label: '\u0425' - base, capslock+shift: '\u0445' + base: '\u0445' shift, capslock: '\u0425' + shift+capslock: '\u0445' } key J { label: '\u0408' - base, capslock+shift: '\u0458' + base: '\u0458' shift, capslock: '\u0408' + shift+capslock: '\u0458' } key K { label: '\u041a' - base, capslock+shift: '\u043a' + base: '\u043a' shift, capslock: '\u041a' + shift+capslock: '\u043a' } key L { label: '\u041b' - base, capslock+shift: '\u043b' + base: '\u043b' shift, capslock: '\u041b' + shift+capslock: '\u043b' } key SEMICOLON { label: '\u0427' - base, capslock+shift: '\u0447' + base: '\u0447' shift, capslock: '\u0427' + shift+capslock: '\u0447' } key APOSTROPHE { label: '\u040b' - base, capslock+shift: '\u045b' + base: '\u045b' shift, capslock: '\u040b' + shift+capslock: '\u045b' } key BACKSLASH { label: '\u0416' - base, capslock+shift: '\u0436' + base: '\u0436' shift, capslock: '\u0416' + shift+capslock: '\u0436' } ### ROW 4 @@ -259,44 +283,51 @@ key PLUS { key Z { label: '\u0405' - base, capslock+shift: '\u0455' + base: '\u0455' shift, capslock: '\u0405' + shift+capslock: '\u0455' } key X { label: '\u040f' - base, capslock+shift: '\u045f' + base: '\u045f' shift, capslock: '\u040f' + shift+capslock: '\u045f' } key C { label: '\u0426' - base, capslock+shift: '\u0446' + base: '\u0446' shift, capslock: '\u0426' + shift+capslock: '\u0446' } key V { label: '\u0412' - base, capslock+shift: '\u0432' + base: '\u0432' shift, capslock: '\u0412' + shift+capslock: '\u0432' } key B { label: '\u0411' - base, capslock+shift: '\u0431' + base: '\u0431' shift, capslock: '\u0411' + shift+capslock: '\u0431' } key N { label: '\u041d' - base, capslock+shift: '\u043d' + base: '\u043d' shift, capslock: '\u041d' + shift+capslock: '\u043d' } key M { label: '\u041c' - base, capslock+shift: '\u043c' + base: '\u043c' shift, capslock: '\u041c' + shift+capslock: '\u043c' } key COMMA { @@ -317,4 +348,4 @@ key SLASH { label: '-' base: '-' shift: '_' -}
\ No newline at end of file +} diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm index 8e4d7b147faa..4c8997b16a26 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm @@ -120,78 +120,90 @@ key EQUALS { key Q { label: 'Q' - base, capslock+shift: 'q' + base: 'q' shift, capslock: 'Q' + shift+capslock: 'q' ralt: '\\' } key W { label: 'W' - base, capslock+shift: 'w' + base: 'w' shift, capslock: 'W' + shift+capslock: 'w' ralt: '|' } key E { label: 'E' - base, capslock+shift: 'e' + base: 'e' shift, capslock: 'E' + shift+capslock: 'e' ralt: '\u20ac' } key R { label: 'R' - base, capslock+shift: 'r' + base: 'r' shift, capslock: 'R' + shift+capslock: 'r' } key T { label: 'T' - base, capslock+shift: 't' + base: 't' shift, capslock: 'T' + shift+capslock: 't' } key Z { label: 'Z' - base, capslock+shift: 'z' + base: 'z' shift, capslock: 'Z' + shift+capslock: 'z' } key U { label: 'U' - base, capslock+shift: 'u' + base: 'u' shift, capslock: 'U' + shift+capslock: 'u' } key I { label: 'I' - base, capslock+shift: 'i' + base: 'i' shift, capslock: 'I' + shift+capslock: 'i' } key O { label: 'O' - base, capslock+shift: 'o' + base: 'o' shift, capslock: 'O' + shift+capslock: 'o' } key P { label: 'P' - base, capslock+shift: 'p' + base: 'p' shift, capslock: 'P' + shift+capslock: 'p' } key LEFT_BRACKET { label: '\u0160' - base, capslock+shift: '\u0161' + base: '\u0161' shift, capslock: '\u0160' + shift+capslock: '\u0161' ralt: '\u00f7' } key RIGHT_BRACKET { label: '\u0110' - base, capslock+shift: '\u0111' + base: '\u0111' shift, capslock: '\u0110' + shift+capslock: '\u0111' ralt: '\u00d7' } @@ -199,79 +211,91 @@ key RIGHT_BRACKET { key A { label: 'A' - base, capslock+shift: 'a' + base: 'a' shift, capslock: 'A' + shift+capslock: 'a' } key S { label: 'S' - base, capslock+shift: 's' + base: 's' shift, capslock: 'S' + shift+capslock: 's' } key D { label: 'D' - base, capslock+shift: 'd' + base: 'd' shift, capslock: 'D' + shift+capslock: 'd' } key F { label: 'F' - base, capslock+shift: 'f' + base: 'f' shift, capslock: 'F' + shift+capslock: 'f' ralt: '[' } key G { label: 'G' - base, capslock+shift: 'g' + base: 'g' shift, capslock: 'G' + shift+capslock: 'g' ralt: ']' } key H { label: 'H' - base, capslock+shift: 'h' + base: 'h' shift, capslock: 'H' + shift+capslock: 'h' } key J { label: 'J' - base, capslock+shift: 'j' + base: 'j' shift, capslock: 'J' + shift+capslock: 'j' } key K { label: 'K' - base, capslock+shift: 'k' + base: 'k' shift, capslock: 'K' + shift+capslock: 'k' ralt: '\u0142' } key L { label: 'L' - base, capslock+shift: 'l' + base: 'l' shift, capslock: 'L' + shift+capslock: 'l' ralt: '\u0141' } key SEMICOLON { label: '\u010c' - base, capslock+shift: '\u010d' + base: '\u010d' shift, capslock: '\u010c' + shift+capslock: '\u010d' } key APOSTROPHE { label: '\u0106' - base, capslock+shift: '\u0107' + base: '\u0107' shift, capslock: '\u0106' + shift+capslock: '\u0107' ralt: '\u00df' } key BACKSLASH { label: '\u017d' - base, capslock+shift: '\u017e' + base: '\u017e' shift, capslock: '\u017d' + shift+capslock: '\u017e' ralt: '\u00a4' } @@ -285,47 +309,54 @@ key PLUS { key Y { label: 'Y' - base, capslock+shift: 'y' + base: 'y' shift, capslock: 'Y' + shift+capslock: 'y' } key X { label: 'X' - base, capslock+shift: 'x' + base: 'x' shift, capslock: 'X' + shift+capslock: 'x' } key C { label: 'C' - base, capslock+shift: 'c' + base: 'c' shift, capslock: 'C' + shift+capslock: 'c' } key V { label: 'V' - base, capslock+shift: 'v' + base: 'v' shift, capslock: 'V' + shift+capslock: 'v' ralt: '@' } key B { label: 'B' - base, capslock+shift: 'b' + base: 'b' shift, capslock: 'B' + shift+capslock: 'b' ralt: '{' } key N { label: 'N' - base, capslock+shift: 'n' + base: 'n' shift, capslock: 'N' + shift+capslock: 'n' ralt: '}' } key M { label: 'M' - base, capslock+shift: 'm' + base: 'm' shift, capslock: 'M' + shift+capslock: 'm' ralt: '\u00a7' } @@ -347,4 +378,4 @@ key MINUS { label: '-' base: '-' shift: '_' -}
\ No newline at end of file +} diff --git a/packages/PrintSpooler/Android.bp b/packages/PrintSpooler/Android.bp index 6af3c6624f62..000e20fb4280 100644 --- a/packages/PrintSpooler/Android.bp +++ b/packages/PrintSpooler/Android.bp @@ -59,6 +59,21 @@ android_library { "android-support-core-ui", "android-support-fragment", "android-support-annotations", + "printspooler_aconfig_flags_java_lib", ], manifest: "AndroidManifest.xml", } + +aconfig_declarations { + name: "printspooler_aconfig_declarations", + package: "com.android.printspooler.flags", + container: "system", + srcs: [ + "flags/flags.aconfig", + ], +} + +java_aconfig_library { + name: "printspooler_aconfig_flags_java_lib", + aconfig_declarations: "printspooler_aconfig_declarations", +} diff --git a/packages/PrintSpooler/flags/flags.aconfig b/packages/PrintSpooler/flags/flags.aconfig new file mode 100644 index 000000000000..4a76dff405d0 --- /dev/null +++ b/packages/PrintSpooler/flags/flags.aconfig @@ -0,0 +1,9 @@ +package: "com.android.printspooler.flags" +container: "system" + +flag { + name: "log_print_jobs" + namespace: "printing" + description: "Log print job creation and state transitions." + bug: "385340868" +} diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java index bba57d5fe0a2..1a9309c13bd7 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java @@ -68,6 +68,7 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.dump.DualDumpOutputStream; import com.android.internal.util.function.pooled.PooledLambda; import com.android.printspooler.R; +import com.android.printspooler.flags.Flags; import com.android.printspooler.util.ApprovedPrintServices; import libcore.io.IoUtils; @@ -493,7 +494,7 @@ public final class PrintSpoolerService extends Service { keepAwakeLocked(); } - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[ADD] " + printJob); } } @@ -506,7 +507,7 @@ public final class PrintSpoolerService extends Service { PrintJobInfo printJob = mPrintJobs.get(i); if (isObsoleteState(printJob.getState())) { mPrintJobs.remove(i); - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[REMOVE] " + printJob.getId().flattenToString()); } removePrintJobFileLocked(printJob.getId()); @@ -568,7 +569,7 @@ public final class PrintSpoolerService extends Service { checkIfStillKeepAwakeLocked(); } - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[STATE CHANGED] " + printJob); } diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt index 145fabea52af..ac36b08512e8 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt @@ -39,5 +39,7 @@ annotation class DataChangeReason { const val RESTORE = 3 /** Data is synced from another profile (e.g. personal profile to work profile). */ const val SYNC_ACROSS_PROFILES = 4 + + fun isDataChange(reason: Int): Boolean = reason in UNKNOWN..SYNC_ACROSS_PROFILES } } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt index 3c870acf2291..ea795542a5f6 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -34,6 +34,8 @@ import com.android.settingslib.metadata.PreferenceRestrictionProvider import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.RangeValue import com.android.settingslib.metadata.ReadWritePermit +import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY +import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY /** Request to set preference value. */ data class PreferenceSetterRequest( @@ -187,6 +189,8 @@ fun <T> PersistentPreference<T>.evalWritePermit( callingUid: Int, ): Int = when { + sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY -> + ReadWritePermit.DISALLOW getWritePermissions(context)?.check(context, callingPid, callingUid) == false -> ReadWritePermit.REQUIRE_APP_PERMISSION else -> getWritePermit(context, value, callingPid, callingUid) diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt index e5bf41f87999..83725aaec377 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt @@ -44,6 +44,24 @@ annotation class ReadWritePermit { } } +/** The reason of preference change. */ +@IntDef( + PreferenceChangeReason.VALUE, + PreferenceChangeReason.STATE, + PreferenceChangeReason.DEPENDENT, +) +@Retention(AnnotationRetention.SOURCE) +annotation class PreferenceChangeReason { + companion object { + /** Preference value is changed. */ + const val VALUE = 1000 + /** Preference state (title/summary, enable state, etc.) is changed. */ + const val STATE = 1001 + /** Dependent preference state is changed. */ + const val DEPENDENT = 1002 + } +} + /** Indicates how sensitive of the data. */ @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.TYPE) diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt index 91abd8b4c9e9..8358ab921fb6 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt @@ -25,12 +25,14 @@ import androidx.preference.Preference import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceGroup import androidx.preference.PreferenceScreen +import com.android.settingslib.datastore.DataChangeReason import com.android.settingslib.datastore.HandlerExecutor import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyedDataObservable import com.android.settingslib.datastore.KeyedObservable import com.android.settingslib.datastore.KeyedObserver import com.android.settingslib.metadata.PersistentPreference +import com.android.settingslib.metadata.PreferenceChangeReason import com.android.settingslib.metadata.PreferenceHierarchy import com.android.settingslib.metadata.PreferenceHierarchyNode import com.android.settingslib.metadata.PreferenceLifecycleContext @@ -73,7 +75,7 @@ class PreferenceScreenBindingHelper( ?.keyValueStore override fun notifyPreferenceChange(key: String) = - notifyChange(key, CHANGE_REASON_STATE) + notifyChange(key, PreferenceChangeReason.STATE) @Suppress("DEPRECATION") override fun startActivityForResult( @@ -91,7 +93,13 @@ class PreferenceScreenBindingHelper( private val preferenceObserver: KeyedObserver<String?> private val storageObserver = - KeyedObserver<String> { key, _ -> notifyChange(key, CHANGE_REASON_VALUE) } + KeyedObserver<String> { key, reason -> + if (DataChangeReason.isDataChange(reason)) { + notifyChange(key, PreferenceChangeReason.VALUE) + } else { + notifyChange(key, PreferenceChangeReason.STATE) + } + } init { val preferencesBuilder = ImmutableMap.builder<String, PreferenceHierarchyNode>() @@ -148,7 +156,7 @@ class PreferenceScreenBindingHelper( } // check reason to avoid potential infinite loop - if (reason != CHANGE_REASON_DEPENDENT) { + if (reason != PreferenceChangeReason.DEPENDENT) { notifyDependents(key, mutableSetOf()) } } @@ -157,7 +165,7 @@ class PreferenceScreenBindingHelper( private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) { if (!notifiedKeys.add(key)) return for (dependency in dependencies[key]) { - notifyChange(dependency, CHANGE_REASON_DEPENDENT) + notifyChange(dependency, PreferenceChangeReason.DEPENDENT) notifyDependents(dependency, notifiedKeys) } } @@ -210,13 +218,6 @@ class PreferenceScreenBindingHelper( } companion object { - /** Preference value is changed. */ - const val CHANGE_REASON_VALUE = 0 - /** Preference state (title/summary, enable state, etc.) is changed. */ - const val CHANGE_REASON_STATE = 1 - /** Dependent preference state is changed. */ - const val CHANGE_REASON_DEPENDENT = 2 - /** Updates preference screen that has incomplete hierarchy. */ @JvmStatic fun bind(preferenceScreen: PreferenceScreen) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index 6be4336178eb..155c7e6530aa 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -21,6 +21,7 @@ import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; +import android.annotation.CallbackExecutor; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCsipSetCoordinator; @@ -39,6 +40,7 @@ import com.android.settingslib.R; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; public class LeAudioProfile implements LocalBluetoothProfile { private static final String TAG = "LeAudioProfile"; @@ -317,6 +319,78 @@ public class LeAudioProfile implements LocalBluetoothProfile { return mService.getAudioLocation(device); } + /** + * Sets the fallback group id when broadcast switches to unicast. + * + * @param groupId the target fallback group id + */ + public void setBroadcastToUnicastFallbackGroup(int groupId) { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot set fallback group: " + groupId); + return; + } + + mService.setBroadcastToUnicastFallbackGroup(groupId); + } + + /** + * Gets the fallback group id when broadcast switches to unicast. + * + * @return current fallback group id + */ + public int getBroadcastToUnicastFallbackGroup() { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot get fallback group."); + return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; + } + return mService.getBroadcastToUnicastFallbackGroup(); + } + + /** + * Registers a {@link BluetoothLeAudio.Callback} that will be invoked during the + * operation of this profile. + * + * Repeated registration of the same <var>callback</var> object after the first call to this + * method will result with IllegalArgumentException being thrown, even when the + * <var>executor</var> is different. API caller would have to call + * {@link #unregisterCallback(BluetoothLeAudio.Callback)} with the same callback object + * before registering it again. + * + * @param executor an {@link Executor} to execute given callback + * @param callback user implementation of the {@link BluetoothLeAudio.Callback} + * @throws NullPointerException if a null executor, or callback is given, or + * IllegalArgumentException if the same <var>callback</var> is + * already registered. + */ + public void registerCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull BluetoothLeAudio.Callback callback) { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot register callback."); + return; + } + mService.registerCallback(executor, callback); + } + + /** + * Unregisters the specified {@link BluetoothLeAudio.Callback}. + * <p>The same {@link BluetoothLeAudio.Callback} object used when calling + * {@link #registerCallback(Executor, BluetoothLeAudio.Callback)} must be used. + * + * <p>Callbacks are automatically unregistered when application process goes away + * + * @param callback user implementation of the {@link BluetoothLeAudio.Callback} + * @throws NullPointerException when callback is null or IllegalArgumentException when no + * callback is registered + */ + public void unregisterCallback(@NonNull BluetoothLeAudio.Callback callback) { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot unregister callback."); + return; + } + mService.unregisterCallback(callback); + } + @RequiresApi(Build.VERSION_CODES.S) protected void finalize() { if (DEBUG) { diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java index dc40304ba24a..51259e2f311d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java @@ -16,6 +16,8 @@ package com.android.settingslib.dream; +import static android.service.dreams.Flags.allowDreamWhenPostured; + import android.annotation.IntDef; import android.content.ComponentName; import android.content.Context; @@ -78,14 +80,21 @@ public class DreamBackend { } @Retention(RetentionPolicy.SOURCE) - @IntDef({WHILE_CHARGING, WHILE_DOCKED, EITHER, NEVER}) + @IntDef({ + WHILE_CHARGING, + WHILE_DOCKED, + WHILE_POSTURED, + WHILE_CHARGING_OR_DOCKED, + NEVER + }) public @interface WhenToDream { } public static final int WHILE_CHARGING = 0; public static final int WHILE_DOCKED = 1; - public static final int EITHER = 2; - public static final int NEVER = 3; + public static final int WHILE_POSTURED = 2; + public static final int WHILE_CHARGING_OR_DOCKED = 3; + public static final int NEVER = 4; /** * The type of dream complications which can be provided by a @@ -134,6 +143,8 @@ public class DreamBackend { .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_CHARGING_ONLY; private static final int WHEN_TO_DREAM_DOCKED = FrameworkStatsLog .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_DOCKED_ONLY; + private static final int WHEN_TO_DREAM_POSTURED = FrameworkStatsLog + .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_POSTURED_ONLY; private static final int WHEN_TO_DREAM_CHARGING_OR_DOCKED = FrameworkStatsLog .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_EITHER_CHARGING_OR_DOCKED; @@ -143,6 +154,7 @@ public class DreamBackend { private final boolean mDreamsEnabledByDefault; private final boolean mDreamsActivatedOnSleepByDefault; private final boolean mDreamsActivatedOnDockByDefault; + private final boolean mDreamsActivatedOnPosturedByDefault; private final Set<ComponentName> mDisabledDreams; private final List<String> mLoggableDreamPrefixes; private Set<Integer> mSupportedComplications; @@ -168,6 +180,8 @@ public class DreamBackend { com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault); mDreamsActivatedOnDockByDefault = resources.getBoolean( com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault); + mDreamsActivatedOnPosturedByDefault = resources.getBoolean( + com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault); mDisabledDreams = Arrays.stream(resources.getStringArray( com.android.internal.R.array.config_disabledDreamComponents)) .map(ComponentName::unflattenFromString) @@ -280,10 +294,11 @@ public class DreamBackend { @WhenToDream public int getWhenToDreamSetting() { - return isActivatedOnDock() && isActivatedOnSleep() ? EITHER + return isActivatedOnDock() && isActivatedOnSleep() ? WHILE_CHARGING_OR_DOCKED : isActivatedOnDock() ? WHILE_DOCKED - : isActivatedOnSleep() ? WHILE_CHARGING - : NEVER; + : isActivatedOnPostured() ? WHILE_POSTURED + : isActivatedOnSleep() ? WHILE_CHARGING + : NEVER; } public void setWhenToDream(@WhenToDream int whenToDream) { @@ -293,16 +308,25 @@ public class DreamBackend { case WHILE_CHARGING: setActivatedOnDock(false); setActivatedOnSleep(true); + setActivatedOnPostured(false); break; case WHILE_DOCKED: setActivatedOnDock(true); setActivatedOnSleep(false); + setActivatedOnPostured(false); break; - case EITHER: + case WHILE_CHARGING_OR_DOCKED: setActivatedOnDock(true); setActivatedOnSleep(true); + setActivatedOnPostured(false); + break; + + case WHILE_POSTURED: + setActivatedOnPostured(true); + setActivatedOnSleep(false); + setActivatedOnDock(false); break; case NEVER: @@ -407,6 +431,22 @@ public class DreamBackend { setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, value); } + public boolean isActivatedOnPostured() { + return allowDreamWhenPostured() + && getBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + mDreamsActivatedOnPosturedByDefault); + } + + /** + * Sets whether dreams should be activated when the device is postured (stationary and upright) + */ + public void setActivatedOnPostured(boolean value) { + if (allowDreamWhenPostured()) { + logd("setActivatedOnPostured(%s)", value); + setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, value); + } + } + private boolean getBoolean(String key, boolean def) { return Settings.Secure.getInt(mContext.getContentResolver(), key, def ? 1 : 0) == 1; } @@ -548,7 +588,9 @@ public class DreamBackend { return WHEN_TO_DREAM_CHARGING; case WHILE_DOCKED: return WHEN_TO_DREAM_DOCKED; - case EITHER: + case WHILE_POSTURED: + return WHEN_TO_DREAM_POSTURED; + case WHILE_CHARGING_OR_DOCKED: return WHEN_TO_DREAM_CHARGING_OR_DOCKED; case NEVER: default: diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 496c3e6c74cc..9aaefe47fda2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -37,7 +37,8 @@ class FakeZenModeRepository : ZenModeRepository { override val globalZenMode: StateFlow<Int> get() = mutableZenMode.asStateFlow() - private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf()) + private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = + MutableStateFlow(listOf(TestModeBuilder.MANUAL_DND)) override val modes: Flow<List<ZenMode>> get() = mutableModesFlow.asStateFlow() @@ -65,8 +66,11 @@ class FakeZenModeRepository : ZenModeRepository { mutableModesFlow.value += mode } - fun addMode(id: String, @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN, - active: Boolean = false) { + fun addMode( + id: String, + @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN, + active: Boolean = false, + ) { mutableModesFlow.value += newMode(id, type, active) } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index abc163867248..64a2de5025de 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -31,7 +31,6 @@ import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Random; @@ -44,22 +43,7 @@ public class TestModeBuilder { private boolean mIsManualDnd; public static final ZenMode EXAMPLE = new TestModeBuilder().build(); - - public static final ZenMode MANUAL_DND_ACTIVE = manualDnd( - INTERRUPTION_FILTER_PRIORITY, true); - - public static final ZenMode MANUAL_DND_INACTIVE = manualDnd( - INTERRUPTION_FILTER_PRIORITY, false); - - @NonNull - public static ZenMode manualDnd(@NotificationManager.InterruptionFilter int filter, - boolean isActive) { - return new TestModeBuilder() - .makeManualDnd() - .setInterruptionFilter(filter) - .setActive(isActive) - .build(); - } + public static final ZenMode MANUAL_DND = new TestModeBuilder().makeManualDnd().build(); public TestModeBuilder() { // Reasonable defaults diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java index d08d91d18b27..6b30f159129e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java @@ -87,7 +87,7 @@ public class ZenModeTest { @Test public void testBasicMethods_manualDnd() { - ZenMode manualMode = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualMode = TestModeBuilder.MANUAL_DND; assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID); assertThat(manualMode.isManualDnd()).isTrue(); @@ -271,7 +271,7 @@ public class ZenModeTest { @Test public void setInterruptionFilter_manualDnd_throws() { - ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualDnd = TestModeBuilder.MANUAL_DND; assertThrows(IllegalStateException.class, () -> manualDnd.setInterruptionFilter(INTERRUPTION_FILTER_ALL)); @@ -280,24 +280,46 @@ public class ZenModeTest { @Test public void canEditPolicy_onlyFalseForSpecialDnd() { assertThat(TestModeBuilder.EXAMPLE.canEditPolicy()).isTrue(); - assertThat(TestModeBuilder.MANUAL_DND_ACTIVE.canEditPolicy()).isTrue(); - assertThat(TestModeBuilder.MANUAL_DND_INACTIVE.canEditPolicy()).isTrue(); - ZenMode dndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true); + ZenMode inactiveDnd = new TestModeBuilder().makeManualDnd().setActive(false).build(); + assertThat(inactiveDnd.canEditPolicy()).isTrue(); + + ZenMode activeDnd = new TestModeBuilder().makeManualDnd().setActive(true).build(); + assertThat(activeDnd.canEditPolicy()).isTrue(); + + ZenMode dndWithAlarms = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(true) + .build(); assertThat(dndWithAlarms.canEditPolicy()).isFalse(); - ZenMode dndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, true); + + ZenMode dndWithNone = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(true) + .build(); assertThat(dndWithNone.canEditPolicy()).isFalse(); // Note: Backend will never return an inactive manual mode with custom filter. - ZenMode badDndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, false); + ZenMode badDndWithAlarms = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(false) + .build(); assertThat(badDndWithAlarms.canEditPolicy()).isFalse(); - ZenMode badDndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, false); + + ZenMode badDndWithNone = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(false) + .build(); assertThat(badDndWithNone.canEditPolicy()).isFalse(); } @Test public void canEditPolicy_whenTrue_allowsSettingPolicyAndEffects() { - ZenMode normalDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_PRIORITY, true); + ZenMode normalDnd = new TestModeBuilder().makeManualDnd().setActive(true).build(); assertThat(normalDnd.canEditPolicy()).isTrue(); @@ -313,7 +335,11 @@ public class ZenModeTest { @Test public void canEditPolicy_whenFalse_preventsSettingFilterPolicyOrEffects() { - ZenMode specialDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true); + ZenMode specialDnd = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(true) + .build(); assertThat(specialDnd.canEditPolicy()).isFalse(); assertThrows(IllegalStateException.class, @@ -324,7 +350,7 @@ public class ZenModeTest { @Test public void comparator_prioritizes() { - ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualDnd = TestModeBuilder.MANUAL_DND; ZenMode driving1 = new TestModeBuilder().setName("b1").setType(TYPE_DRIVING).build(); ZenMode driving2 = new TestModeBuilder().setName("b2").setType(TYPE_DRIVING).build(); ZenMode bedtime1 = new TestModeBuilder().setName("c1").setType(TYPE_BEDTIME).build(); @@ -403,7 +429,7 @@ public class ZenModeTest { @Test public void getIconKey_manualDnd_isDndIcon() { - ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND_INACTIVE.getIconKey(); + ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND.getIconKey(); assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index dd28402d705f..7b4a2ca5de39 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -155,6 +155,7 @@ public class SecureSettings { Settings.Secure.SCREENSAVER_COMPONENTS, Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index b01f6229af16..b0309a8fa5a5 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -230,6 +230,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.SCREENSAVER_COMPONENTS, COMMA_SEPARATED_COMPONENT_LIST_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_DOCK, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 5ad4b8a6dffe..1c6d6816e9b4 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2550,6 +2550,9 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.Secure.SCREENSAVER_DEFAULT_COMPONENT, SecureSettingsProto.Screensaver.DEFAULT_COMPONENT); + dumpSetting(s, p, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + SecureSettingsProto.Screensaver.ACTIVATE_ON_POSTURED); p.end(screensaverToken); final long searchToken = p.start(SecureSettingsProto.SEARCH); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 7d5fd903c01b..9982710737ce 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -424,24 +424,6 @@ flag { } flag { - name: "status_bar_use_repos_for_call_chip" - namespace: "systemui" - description: "Use repositories as the source of truth for call notifications shown as a chip in" - "the status bar" - bug: "328584859" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - name: "status_bar_call_chip_notification_icon" - namespace: "systemui" - description: "Use the small icon set on the notification for the status bar call chip" - bug: "354930838" -} - -flag { name: "status_bar_signal_policy_refactor" namespace: "systemui" description: "Use a settings observer for airplane mode and make StatusBarSignalPolicy startable" @@ -1344,6 +1326,13 @@ flag { } flag { + name: "output_switcher_redesign" + namespace: "systemui" + description: "Enables visual update for Media Output Switcher" + bug: "388296370" +} + +flag { namespace: "systemui" name: "enable_view_capture_tracing" description: "Enables view capture tracing in System UI." @@ -1806,16 +1795,6 @@ flag { } flag { - name: "disable_shade_expands_on_trackpad_two_finger_swipe" - namespace: "systemui" - description: "Disables expansion of the shade via two finger swipe on a trackpad" - bug: "356804470" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "keyboard_shortcut_helper_shortcut_customizer" namespace: "systemui" description: "An implementation of shortcut customizations through shortcut helper." @@ -1916,6 +1895,16 @@ flag { } flag { + name: "disable_shade_trackpad_two_finger_swipe" + namespace: "systemui" + description: "Disables expansion of the shade via two finger swipe on a trackpad" + bug: "356804470" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notification_magic_actions_treatment" namespace: "systemui" description: "Special UI treatment for magic actions" diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt index 96401ce6e1c7..a27bf8af1806 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt @@ -444,7 +444,7 @@ private class NestedDraggableNode( val left = available - consumed val postConsumed = nestedScrollDispatcher.dispatchPostScroll( - consumed = preConsumed + consumed, + consumed = consumed, available = left, source = NestedScrollSource.UserInput, ) @@ -482,10 +482,9 @@ private class NestedDraggableNode( val available = velocity - preConsumed val consumed = performFling(available) val left = available - consumed - return nestedScrollDispatcher.dispatchPostFling( - consumed = consumed + preConsumed, - available = left, - ) + val postConsumed = + nestedScrollDispatcher.dispatchPostFling(consumed = consumed, available = left) + return preConsumed + consumed + postConsumed } /* @@ -549,9 +548,10 @@ private class NestedDraggableNode( nestedScrollController == null && // TODO(b/388231324): Remove this. !lastEventWasScrollWheel && - draggable.shouldConsumeNestedScroll(sign) + draggable.shouldConsumeNestedScroll(sign) && + lastFirstDown != null ) { - val startedPosition = checkNotNull(lastFirstDown) { "lastFirstDown is not set" } + val startedPosition = checkNotNull(lastFirstDown) // TODO(b/382665591): Ensure that there is at least one pointer down. val pointersDownCount = pointersDown.size.coerceAtLeast(1) diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt index 5de0f1221f0f..19d28cc2d626 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerType @@ -52,6 +53,7 @@ import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.compose.ui.test.swipeLeft +import androidx.compose.ui.test.swipeWithVelocity import androidx.compose.ui.unit.Velocity import com.google.common.truth.Truth.assertThat import kotlin.math.ceil @@ -773,6 +775,181 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3") } + @Test + fun nestedDragNotStartedWhenEnabledAfterDragStarted() { + val draggable = TestDraggable() + var enabled by mutableStateOf(false) + val touchSlop = + rule.setContentWithTouchSlop { + Box( + Modifier.fillMaxSize() + .nestedDraggable(draggable, orientation, enabled = enabled) + .scrollable(rememberScrollableState { 0f }, orientation) + ) + } + + rule.onRoot().performTouchInput { down(center) } + + enabled = true + rule.waitForIdle() + + rule.onRoot().performTouchInput { moveBy((touchSlop + 1f).toOffset()) } + + assertThat(draggable.onDragStartedCalled).isFalse() + } + + @Test + fun availableAndConsumedScrollDeltas() { + val totalScroll = 200f + val consumedByEffectPreScroll = 10f // 200f => 190f + val consumedByConnectionPreScroll = 20f // 190f => 170f + val consumedByScroll = 30f // 170f => 140f + val consumedByConnectionPostScroll = 40f // 140f => 100f + + // Available scroll values that we will check later. + var availableToEffectPreScroll = 0f + var availableToConnectionPreScroll = 0f + var availableToScroll = 0f + var availableToConnectionPostScroll = 0f + var availableToEffectPostScroll = 0f + + val effect = + TestOverscrollEffect( + orientation, + onPreScroll = { + availableToEffectPreScroll = it + consumedByEffectPreScroll + }, + onPostScroll = { + availableToEffectPostScroll = it + it + }, + ) + + val connection = + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + availableToConnectionPreScroll = available.toFloat() + return consumedByConnectionPreScroll.toOffset() + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + assertThat(consumed.toFloat()).isEqualTo(consumedByScroll) + availableToConnectionPostScroll = available.toFloat() + return consumedByConnectionPostScroll.toOffset() + } + } + + val draggable = + TestDraggable( + onDrag = { + availableToScroll = it + consumedByScroll + } + ) + + val touchSlop = + rule.setContentWithTouchSlop { + Box( + Modifier.fillMaxSize() + .nestedScroll(connection) + .nestedDraggable(draggable, orientation, effect) + ) + } + + rule.onRoot().performTouchInput { + down(center) + moveBy((touchSlop + totalScroll).toOffset()) + } + + assertThat(availableToEffectPreScroll).isEqualTo(200f) + assertThat(availableToConnectionPreScroll).isEqualTo(190f) + assertThat(availableToScroll).isEqualTo(170f) + assertThat(availableToConnectionPostScroll).isEqualTo(140f) + assertThat(availableToEffectPostScroll).isEqualTo(100f) + } + + @Test + fun availableAndConsumedVelocities() { + val totalVelocity = 200f + val consumedByEffectPreFling = 10f // 200f => 190f + val consumedByConnectionPreFling = 20f // 190f => 170f + val consumedByFling = 30f // 170f => 140f + val consumedByConnectionPostFling = 40f // 140f => 100f + + // Available velocities that we will check later. + var availableToEffectPreFling = 0f + var availableToConnectionPreFling = 0f + var availableToFling = 0f + var availableToConnectionPostFling = 0f + var availableToEffectPostFling = 0f + + val effect = + TestOverscrollEffect( + orientation, + onPreFling = { + availableToEffectPreFling = it + consumedByEffectPreFling + }, + onPostFling = { + availableToEffectPostFling = it + it + }, + onPostScroll = { 0f }, + ) + + val connection = + object : NestedScrollConnection { + override suspend fun onPreFling(available: Velocity): Velocity { + availableToConnectionPreFling = available.toFloat() + return consumedByConnectionPreFling.toVelocity() + } + + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity, + ): Velocity { + assertThat(consumed.toFloat()).isEqualTo(consumedByFling) + availableToConnectionPostFling = available.toFloat() + return consumedByConnectionPostFling.toVelocity() + } + } + + val draggable = + TestDraggable( + onDragStopped = { velocity, _ -> + availableToFling = velocity + consumedByFling + }, + onDrag = { 0f }, + ) + + rule.setContent { + Box( + Modifier.fillMaxSize() + .nestedScroll(connection) + .nestedDraggable(draggable, orientation, effect) + ) + } + + rule.onRoot().performTouchInput { + when (orientation) { + Orientation.Horizontal -> swipeWithVelocity(topLeft, topRight, totalVelocity) + Orientation.Vertical -> swipeWithVelocity(topLeft, bottomLeft, totalVelocity) + } + } + + assertThat(availableToEffectPreFling).isWithin(1f).of(200f) + assertThat(availableToConnectionPreFling).isWithin(1f).of(190f) + assertThat(availableToFling).isWithin(1f).of(170f) + assertThat(availableToConnectionPostFling).isWithin(1f).of(140f) + assertThat(availableToEffectPostFling).isWithin(1f).of(100f) + } + private fun ComposeContentTestRule.setContentWithTouchSlop( content: @Composable () -> Unit ): Float { diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt index 8bf9c21639f4..0659f9198730 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt @@ -24,6 +24,8 @@ import androidx.compose.ui.unit.Velocity class TestOverscrollEffect( override val orientation: Orientation, + private val onPreScroll: (Float) -> Float = { 0f }, + private val onPreFling: suspend (Float) -> Float = { 0f }, private val onPostFling: suspend (Float) -> Float = { it }, private val onPostScroll: (Float) -> Float, ) : OverscrollEffect, OrientationAware { @@ -36,19 +38,23 @@ class TestOverscrollEffect( source: NestedScrollSource, performScroll: (Offset) -> Offset, ): Offset { - val consumedByScroll = performScroll(delta) - val available = delta - consumedByScroll - val consumedByEffect = onPostScroll(available.toFloat()).toOffset() - return consumedByScroll + consumedByEffect + val consumedByPreScroll = onPreScroll(delta.toFloat()).toOffset() + val availableToScroll = delta - consumedByPreScroll + val consumedByScroll = performScroll(availableToScroll) + val availableToPostScroll = availableToScroll - consumedByScroll + val consumedByPostScroll = onPostScroll(availableToPostScroll.toFloat()).toOffset() + return consumedByPreScroll + consumedByScroll + consumedByPostScroll } override suspend fun applyToFling( velocity: Velocity, performFling: suspend (Velocity) -> Velocity, ) { - val consumedByFling = performFling(velocity) - val available = velocity - consumedByFling - onPostFling(available.toFloat()) + val consumedByPreFling = onPreFling(velocity.toFloat()).toVelocity() + val availableToFling = velocity - consumedByPreFling + val consumedByFling = performFling(availableToFling) + val availableToPostFling = availableToFling - consumedByFling + onPostFling(availableToPostFling.toFloat()) applyToFlingDone = true } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 9c53afecad11..a2a91fcd5d52 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -78,6 +78,18 @@ object TransitionDuration { const val EDIT_MODE_TO_HUB_GRID_END_MS = EDIT_MODE_TO_HUB_GRID_DELAY_MS + EDIT_MODE_TO_HUB_CONTENT_MS const val HUB_TO_EDIT_MODE_CONTENT_MS = 250 + const val TO_GLANCEABLE_HUB_DURATION_MS = 1000 +} + +val sceneTransitionsV2 = transitions { + to(CommunalScenes.Communal) { + spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS) + fade(AllElements) + } + to(CommunalScenes.Blank) { + spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS)) + fade(AllElements) + } } val sceneTransitions = transitions { @@ -157,7 +169,7 @@ fun CommunalContainer( MutableSceneTransitionLayoutState( initialScene = currentSceneKey, canChangeScene = { _ -> viewModel.canChangeScene() }, - transitions = sceneTransitions, + transitions = if (viewModel.v2FlagEnabled()) sceneTransitionsV2 else sceneTransitions, ) } val isUiBlurred by viewModel.isUiBlurred.collectAsStateWithLifecycle() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 6d24fc16df23..aa8b4ae9000d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -48,8 +48,8 @@ import com.android.systemui.shade.ui.composable.Shade val SceneContainerTransitions = transitions { interruptionHandler = SceneContainerInterruptionHandler - // Overscroll progress starts linearly with some resistance (3f) and slowly approaches 0.2f - defaultSwipeSpec = spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f) + defaultMotionSpatialSpec = + spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f) // Scene transitions diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt index 93c10b6224ab..6ca9c7934979 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt @@ -17,17 +17,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.communal.ui.compose.AllElements -import com.android.systemui.communal.ui.compose.Communal fun TransitionBuilder.dreamToCommunalTransition() { spec = tween(durationMillis = 1000) - // Translate communal hub grid from the end direction. - translate(Communal.Elements.Grid, Edge.End) - - // Fade all communal hub elements. - timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) } + // Fade in all communal hub elements. + fade(AllElements) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt index ce7a85b19fb4..e30e7d3ee34c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt @@ -30,7 +30,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.goneToSplitShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt index 826a2550cca3..de9a78c497f8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt @@ -17,21 +17,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.communal.ui.compose.AllElements -import com.android.systemui.communal.ui.compose.Communal -import com.android.systemui.scene.shared.model.Scenes fun TransitionBuilder.lockscreenToCommunalTransition() { spec = tween(durationMillis = 1000) - // Translate lockscreen to the start direction. - translate(Scenes.Lockscreen.rootElementKey, Edge.Start) - - // Translate communal hub grid from the end direction. - translate(Communal.Elements.Grid, Edge.End) - - // Fade all communal hub elements. - timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) } + // Fade all communal hub elements in. + fade(AllElements) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt index 1f7a7380bbc6..1a243ca48157 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt @@ -29,7 +29,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.lockscreenToSplitShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt index 24f285e81da2..a9af95bdcb8a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt @@ -27,7 +27,7 @@ fun TransitionBuilder.notificationsShadeToQuickSettingsShadeTransition( durationScale: Double = 1.0 ) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt index 3d62151baf2f..ddea5854d67e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt @@ -31,7 +31,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt index e78bc6afcc4f..e477a41ac608 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt @@ -28,7 +28,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toQuickSettingsShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt index bfae4897dc68..4db4934cf271 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt @@ -32,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt index ff8efc28aa21..d50304d433f9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -34,7 +34,7 @@ import com.android.internal.jank.Cuj.CujType /** The transitions configuration of a [SceneTransitionLayout]. */ class SceneTransitions internal constructor( - internal val defaultSwipeSpec: SpringSpec<Float>, + internal val defaultMotionSpatialSpec: SpringSpec<Float>, internal val transitionSpecs: List<TransitionSpecImpl>, internal val interruptionHandler: InterruptionHandler, ) { @@ -132,7 +132,7 @@ internal constructor( val Empty = SceneTransitions( - defaultSwipeSpec = DefaultSwipeSpec, + defaultMotionSpatialSpec = DefaultSwipeSpec, transitionSpecs = emptyList(), interruptionHandler = DefaultInterruptionHandler, ) @@ -194,9 +194,9 @@ internal interface TransformationSpec { * The [SpringSpec] used to animate the associated transition progress when the transition was * started by a swipe and is now animating back to a scene because the user lifted their finger. * - * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used. + * If `null`, then the [SceneTransitions.defaultMotionSpatialSpec] will be used. */ - val swipeSpec: SpringSpec<Float>? + val motionSpatialSpec: AnimationSpec<Float>? /** * The distance it takes for this transition to animate from 0% to 100% when it is driven by a @@ -213,7 +213,7 @@ internal interface TransformationSpec { internal val Empty = TransformationSpecImpl( progressSpec = snap(), - swipeSpec = null, + motionSpatialSpec = null, distance = null, transformationMatchers = emptyList(), ) @@ -246,7 +246,7 @@ internal class TransitionSpecImpl( val reverse = transformationSpec.invoke(transition) TransformationSpecImpl( progressSpec = reverse.progressSpec, - swipeSpec = reverse.swipeSpec, + motionSpatialSpec = reverse.motionSpatialSpec, distance = reverse.distance, transformationMatchers = reverse.transformationMatchers.map { @@ -276,7 +276,7 @@ internal class TransitionSpecImpl( */ internal class TransformationSpecImpl( override val progressSpec: AnimationSpec<Float>, - override val swipeSpec: SpringSpec<Float>?, + override val motionSpatialSpec: SpringSpec<Float>?, override val distance: UserActionDistance?, override val transformationMatchers: List<TransformationMatcher>, ) : TransformationSpec { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt index ba92f9bea07d..2bfa0199f30b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt @@ -365,10 +365,10 @@ internal class SwipeAnimation<T : ContentKey>( return 0f } - val swipeSpec = + val motionSpatialSpec = spec - ?: contentTransition.transformationSpec.swipeSpec - ?: layoutState.transitions.defaultSwipeSpec + ?: contentTransition.transformationSpec.motionSpatialSpec + ?: layoutState.transitions.defaultMotionSpatialSpec val velocityConsumed = CompletableDeferred<Float>() @@ -376,7 +376,7 @@ internal class SwipeAnimation<T : ContentKey>( val result = animatable.animateTo( targetValue = targetOffset, - animationSpec = swipeSpec, + animationSpec = motionSpatialSpec, initialVelocity = initialVelocity, ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index 998054ef6c9e..776d553ee49c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -40,7 +40,7 @@ interface SceneTransitionsBuilder { * The default [AnimationSpec] used when after the user lifts their finger after starting a * swipe to transition, to animate back into one of the 2 scenes we are transitioning to. */ - var defaultSwipeSpec: SpringSpec<Float> + var defaultMotionSpatialSpec: SpringSpec<Float> /** * The [InterruptionHandler] used when transitions are interrupted. Defaults to @@ -145,9 +145,9 @@ interface TransitionBuilder : BaseTransitionBuilder { * The [SpringSpec] used to animate the associated transition progress when the transition was * started by a swipe and is now animating back to a scene because the user lifted their finger. * - * If `null`, then the [SceneTransitionsBuilder.defaultSwipeSpec] will be used. + * If `null`, then the [SceneTransitionsBuilder.defaultMotionSpatialSpec] will be used. */ - var swipeSpec: SpringSpec<Float>? + var motionSpatialSpec: SpringSpec<Float>? /** The CUJ associated to this transitions. */ @CujType var cuj: Int? diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 7ca521513714..9a9b05eb3c1d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -41,11 +41,15 @@ import com.android.internal.jank.Cuj.CujType internal fun transitionsImpl(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions { val impl = SceneTransitionsBuilderImpl().apply(builder) - return SceneTransitions(impl.defaultSwipeSpec, impl.transitionSpecs, impl.interruptionHandler) + return SceneTransitions( + defaultMotionSpatialSpec = impl.defaultMotionSpatialSpec, + transitionSpecs = impl.transitionSpecs, + interruptionHandler = impl.interruptionHandler, + ) } private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { - override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec + override var defaultMotionSpatialSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec override var interruptionHandler: InterruptionHandler = DefaultInterruptionHandler val transitionSpecs = mutableListOf<TransitionSpecImpl>() @@ -105,7 +109,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { val impl = TransitionBuilderImpl(transition).apply(builder) return TransformationSpecImpl( progressSpec = impl.spec, - swipeSpec = impl.swipeSpec, + motionSpatialSpec = impl.motionSpatialSpec, distance = impl.distance, transformationMatchers = impl.transformationMatchers, ) @@ -209,7 +213,7 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { internal class TransitionBuilderImpl(override val transition: TransitionState.Transition) : BaseTransitionBuilderImpl(), TransitionBuilder { override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow) - override var swipeSpec: SpringSpec<Float>? = null + override var motionSpatialSpec: SpringSpec<Float>? = null override var distance: UserActionDistance? = null override var cuj: Int? = null private val durationMillis: Int by lazy { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt index 712af56ee1bc..097722665f8e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt @@ -390,10 +390,10 @@ sealed interface TransitionState { fun create(): Animatable<Float, AnimationVector1D> { val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold) layoutImpl.animationScope.launch { - val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec + val motionSpatialSpec = layoutImpl.state.transitions.defaultMotionSpatialSpec val progressSpec = spring( - stiffness = swipeSpec.stiffness, + stiffness = motionSpatialSpec.stiffness, dampingRatio = Spring.DampingRatioNoBouncy, visibilityThreshold = ProgressVisibilityThreshold, ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt index 7c4dbf153013..00cd0ca564b1 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt @@ -104,7 +104,7 @@ fun TransitionBuilder.verticalContainerReveal( val alphaSpec = spring<Float>(stiffness = 1200f, dampingRatio = 0.99f) // The spring animating the progress when releasing the finger. - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index dbac62ffb713..5a9edba26d13 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -137,13 +137,6 @@ class DraggableHandlerTest { var pointerInfoOwner: () -> PointersInfo = { pointersDown() } - fun nestedScrollConnection() = - NestedScrollHandlerImpl( - draggableHandler = draggableHandler, - pointersInfoOwner = { pointerInfoOwner() }, - ) - .connection - val velocityThreshold = draggableHandler.velocityThreshold fun down(fractionOfScreen: Float) = @@ -607,57 +600,6 @@ class DraggableHandlerTest { } @Test - fun nestedScrollUseFromSourceInfo() = runGestureTest { - // Start at scene C. - navigateToSceneC() - val nestedScroll = nestedScrollConnection() - - // Drag from the **top** of the screen - pointerInfoOwner = { pointersDown() } - assertIdle(currentScene = SceneC) - - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertTransition( - currentScene = SceneC, - fromScene = SceneC, - // userAction: Swipe.Up to SceneB - toScene = SceneB, - progress = 0.1f, - ) - - // Reset to SceneC - nestedScroll.preFling(Velocity.Zero) - advanceUntilIdle() - - // Drag from the **bottom** of the screen - pointerInfoOwner = { pointersDown(startedPosition = Offset(0f, SCREEN_SIZE)) } - assertIdle(currentScene = SceneC) - - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertTransition( - currentScene = SceneC, - fromScene = SceneC, - // userAction: Swipe.Up(fromSource = Edge.Bottom) to SceneA - toScene = SceneA, - progress = 0.1f, - ) - } - - @Test - fun ignoreMouseWheel() = runGestureTest { - // Start at scene C. - navigateToSceneC() - val nestedScroll = nestedScrollConnection() - - // Use mouse wheel - pointerInfoOwner = { PointersInfo.MouseWheel } - assertIdle(currentScene = SceneC) - - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertIdle(currentScene = SceneC) - } - - @Test fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest { // Swipe up from the middle to transition to scene B. val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) @@ -689,28 +631,10 @@ class DraggableHandlerTest { } @Test - fun scrollKeepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() = runGestureTest { - val nestedScroll = nestedScrollConnection() - - // Overscroll is disabled, it will scroll up to 100% - nestedScroll.scroll(available = upOffset(fractionOfScreen = 2f)) - assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f) - - // We need to maintain scroll priority even if the scene transition can no longer consume - // the scroll gesture. - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f) - - // A scroll gesture in the opposite direction allows us to return to the previous scene. - nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.5f)) - assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.5f) - } - - @Test fun overscroll_releaseBetween0And100Percent_up() = runGestureTest { // Make scene B overscrollable. layoutState.transitions = transitions { - defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) + defaultMotionSpatialSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) from(SceneA, to = SceneB) {} } @@ -739,7 +663,7 @@ class DraggableHandlerTest { fun overscroll_releaseBetween0And100Percent_down() = runGestureTest { // Make scene C overscrollable. layoutState.transitions = transitions { - defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) + defaultMotionSpatialSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) from(SceneA, to = SceneC) {} } @@ -944,33 +868,4 @@ class DraggableHandlerTest { assertThat(layoutState.transitionState).hasCurrentScene(SceneA) assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB) } - - @Test - fun replaceOverlayNestedScroll() = runGestureTest { - layoutState.showOverlay(OverlayA, animationScope = testScope) - advanceUntilIdle() - - // Initial state. - assertThat(layoutState.transitionState).isIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA) - - // Swipe down to replace overlay A by overlay B. - - val nestedScroll = nestedScrollConnection() - nestedScroll.scroll(downOffset(0.1f)) - val transition = assertThat(layoutState.transitionState).isReplaceOverlayTransition() - assertThat(transition).hasCurrentScene(SceneA) - assertThat(transition).hasFromOverlay(OverlayA) - assertThat(transition).hasToOverlay(OverlayB) - assertThat(transition).hasCurrentOverlays(OverlayA) - assertThat(transition).hasProgress(0.1f) - - nestedScroll.preFling(Velocity(0f, velocityThreshold)) - advanceUntilIdle() - // Commit the gesture. The overlays are instantly swapped in the set of current overlays. - assertThat(layoutState.transitionState).isIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB) - } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 53495be7b02a..005146997813 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -618,7 +618,7 @@ class ElementTest { fun layoutGetsCurrentTransitionStateFromComposition() { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { @@ -1126,7 +1126,7 @@ class ElementTest { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) } @@ -1331,7 +1331,7 @@ class ElementTest { val fooSize = 100.dp val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) } @@ -1439,7 +1439,7 @@ class ElementTest { @Test fun targetStateIsSetEvenWhenNotPlaced() { // Start directly at A => B but with progress < 0f to overscroll on A. - val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } lateinit var layoutImpl: SceneTransitionLayoutImpl val scope = @@ -1473,7 +1473,7 @@ class ElementTest { fun lastAlphaIsNotSetByOutdatedLayer() { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } }, ) @@ -1537,7 +1537,7 @@ class ElementTest { fun fadingElementsDontAppearInstantly() { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } }, ) @@ -1583,7 +1583,7 @@ class ElementTest { @Test fun lastPlacementValuesAreClearedOnNestedElements() { - val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) } @Composable fun ContentScope.NestedFooBar() { @@ -1658,7 +1658,7 @@ class ElementTest { fun currentTransitionSceneIsUsedToComputeElementValues() { val state = rule.runOnIdle { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneB, to = SceneC) { @@ -1709,7 +1709,7 @@ class ElementTest { @Test fun interruptionDeltasAreProperlyCleaned() { - val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) } @Composable fun ContentScope.Foo(offset: Dp) { @@ -1780,7 +1780,7 @@ class ElementTest { fun transparentElementIsNotImpactingInterruption() { val state = rule.runOnIdle { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { @@ -1856,7 +1856,7 @@ class ElementTest { @Test fun replacedTransitionDoesNotTriggerInterruption() { - val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) } @Composable fun ContentScope.Foo(modifier: Modifier = Modifier) { @@ -2027,7 +2027,7 @@ class ElementTest { ): SceneTransitionLayoutImpl { val state = rule.runOnIdle { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( from, transitions { from(from, to = to, preview = preview, builder = transition) }, ) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index e80805a4e374..0355a30d5c73 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ScrollWheel +import androidx.compose.ui.test.TouchInjectionScope import androidx.compose.ui.test.assertPositionInRootIsEqualTo import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.createComposeRule @@ -55,6 +56,8 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestOverlays.OverlayA +import com.android.compose.animation.scene.TestOverlays.OverlayB import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC @@ -977,4 +980,164 @@ class SwipeToSceneTest { rule.waitForIdle() assertThat(state.transitionState).isSceneTransition() } + + @Test + fun nestedScroll_useFromSourceInfo() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene( + SceneA, + userActions = + mapOf(Swipe.Down to SceneB, Swipe.Down(fromSource = Edge.Top) to SceneC), + ) { + // Use a fullscreen nested scrollable to use the nested scroll connection. + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + scene(SceneC) { Box(Modifier.fillMaxSize()) } + } + } + + // Swiping down from the middle of the screen leads to B. + rule.onRoot().performTouchInput { + down(center) + moveBy(Offset(0f, touchSlop + 1f)) + } + + var transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + + // Release finger and wait to settle back to A. + rule.onRoot().performTouchInput { up() } + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneA) + + // Swiping down from the top of the screen leads to B. + rule.onRoot().performTouchInput { + down(center.copy(y = 0f)) + moveBy(Offset(0f, touchSlop + 1f)) + } + + transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneC) + } + + @Test + fun nestedScroll_ignoreMouseWheel() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { + // Use a fullscreen nested scrollable to use the nested scroll connection. + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + rule.onRoot().performMouseInput { + scroll(-touchSlop - 1f, scrollWheel = ScrollWheel.Vertical) + } + assertThat(state.transitionState).isIdle() + } + + @Test + fun nestedScroll_keepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { + // Use a fullscreen nested scrollable to use the nested scroll connection. + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + fun TouchInjectionScope.height() = bottom + fun TouchInjectionScope.halfHeight() = height() / 2f + + rule.onRoot().performTouchInput { + down(center.copy(y = 0f)) + moveBy(Offset(0f, touchSlop + halfHeight())) + } + val transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasProgress(0.5f, tolerance = 0.01f) + + // The progress should never go above 100%. + rule.onRoot().performTouchInput { moveBy(Offset(0f, height())) } + assertThat(transition).hasProgress(1f, tolerance = 0.01f) + + // Because the overscroll effect of scene B is not attached, swiping in the opposite + // direction will directly decrease the progress. + rule.onRoot().performTouchInput { moveBy(Offset(0f, -halfHeight())) } + assertThat(transition).hasProgress(0.5f, tolerance = 0.01f) + } + + @Test + fun nestedScroll_replaceOverlay() { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA)) + } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene(SceneA) { Box(Modifier.fillMaxSize()) } + overlay( + OverlayA, + mapOf(Swipe.Down to UserActionResult.ReplaceByOverlay(OverlayB)), + ) { + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + overlay(OverlayB) { Box(Modifier.fillMaxSize()) } + } + } + + // Swipe down 100% to replace A by B. + rule.onRoot().performTouchInput { + down(center.copy(y = 0f)) + moveBy(Offset(0f, touchSlop + bottom)) + } + + val transition = assertThat(state.transitionState).isReplaceOverlayTransition() + assertThat(transition).hasCurrentScene(SceneA) + assertThat(transition).hasFromOverlay(OverlayA) + assertThat(transition).hasToOverlay(OverlayB) + assertThat(transition).hasCurrentOverlays(OverlayA) + assertThat(transition).hasProgress(1f, tolerance = 0.01f) + + // Commit the gesture. The overlays are instantly swapped in the set of current overlays. + rule.onRoot().performTouchInput { up() } + assertThat(transition).hasCurrentScene(SceneA) + assertThat(transition).hasCurrentOverlays(OverlayB) + + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneA) + assertThat(state.transitionState).hasCurrentOverlays(OverlayB) + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt index cb87fe849a81..aada4a50c89c 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt @@ -276,22 +276,22 @@ class TransitionDslTest { val defaultSpec = spring<Float>(stiffness = 1f) val specFromAToC = spring<Float>(stiffness = 2f) val transitions = transitions { - defaultSwipeSpec = defaultSpec + defaultMotionSpatialSpec = defaultSpec from(SceneA, to = SceneB) { // Default swipe spec. } - from(SceneA, to = SceneC) { swipeSpec = specFromAToC } + from(SceneA, to = SceneC) { motionSpatialSpec = specFromAToC } } - assertThat(transitions.defaultSwipeSpec).isSameInstanceAs(defaultSpec) + assertThat(transitions.defaultMotionSpatialSpec).isSameInstanceAs(defaultSpec) // A => B does not have a custom spec. assertThat( transitions .transitionSpec(from = SceneA, to = SceneB, key = null) .transformationSpec(aToB()) - .swipeSpec + .motionSpatialSpec ) .isNull() @@ -300,7 +300,7 @@ class TransitionDslTest { transitions .transitionSpec(from = SceneA, to = SceneC, key = null) .transformationSpec(transition(from = SceneA, to = SceneC)) - .swipeSpec + .motionSpatialSpec ) .isSameInstanceAs(specFromAToC) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index aed3a2df0436..e69fa994931d 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -50,7 +50,6 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController DEFAULT_CLOCK_ID, clockCtx.resources.getString(R.string.clock_default_name), clockCtx.resources.getString(R.string.clock_default_description), - isReactiveToTone = true, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java index 266591028efb..6edf94939010 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java @@ -65,7 +65,7 @@ public class DragToInteractAnimationControllerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings(); + final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, mockSecureSettings, mHearingAidDeviceManager); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java index 241da5fbc444..15afd2559d9d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java @@ -71,7 +71,7 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { private AccessibilityManager mAccessibilityManager; @Mock private HearingAidDeviceManager mHearingAidDeviceManager; - private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(); + private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(mContext); private RecyclerView mStubListView; private MenuView mMenuView; private MenuViewLayer mMenuViewLayer; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index 715c40a31632..56a97bb34172 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -89,7 +89,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager windowManager = mContext.getSystemService(WindowManager.class); - final SecureSettings secureSettings = TestUtils.mockSecureSettings(); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, secureSettings, mHearingAidDeviceManager); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java index cb7c205742fc..5ff7bd063427 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java @@ -91,7 +91,7 @@ public class MenuViewTest extends SysuiTestCase { mSpyContext = spy(mContext); doNothing().when(mSpyContext).startActivity(any()); - final SecureSettings secureSettings = TestUtils.mockSecureSettings(); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, secureSettings, mHearingAidDeviceManager); final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java index 8399fa85bfb1..aafb21209468 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.ComponentName; +import android.content.Context; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; @@ -76,8 +77,10 @@ public class TestUtils { * Returns a mock secure settings configured to return information needed for tests. * Currently, this only includes button targets. */ - public static SecureSettings mockSecureSettings() { + public static SecureSettings mockSecureSettings(Context context) { SecureSettings secureSettings = mock(SecureSettings.class); + when(secureSettings.getRealUserHandle(UserHandle.USER_CURRENT)) + .thenReturn(context.getUserId()); final String targets = getShortcutTargets( Set.of(TEST_COMPONENT_A, TEST_COMPONENT_B)); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index 20d66155e5ca..6c955bf1818d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -256,6 +256,22 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @EnableSceneContainer @Test + fun playSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() = + testScope.runTest { + underTest = kosmos.deviceEntryHapticsInteractor + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + + enrollFace() + kosmos.configureKeyguardBypass(isBypassAvailable = false) + runCurrent() + configureDeviceEntryFromBiometricSource(isFaceUnlock = true, bypassEnabled = false) + kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true + + assertThat(playSuccessHaptic).isNotNull() + } + + @EnableSceneContainer + @Test fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerDown_sceneContainer() = testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) @@ -299,6 +315,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { private fun configureDeviceEntryFromBiometricSource( isFpUnlock: Boolean = false, isFaceUnlock: Boolean = false, + bypassEnabled: Boolean = true, ) { // Mock DeviceEntrySourceInteractor#deviceEntryBiometricAuthSuccessState if (isFpUnlock) { @@ -314,11 +331,14 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { ) // Mock DeviceEntrySourceInteractor#faceWakeAndUnlockMode = MODE_UNLOCK_COLLAPSING - kosmos.sceneInteractor.setTransitionState( - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(Scenes.Lockscreen) + // if the successful face authentication will bypass keyguard + if (bypassEnabled) { + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(Scenes.Lockscreen) + ) ) - ) + } } underTest = kosmos.deviceEntryHapticsInteractor } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt index fde9b8ce6a50..bf49186a7f01 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt @@ -28,7 +28,7 @@ import android.provider.Settings.Secure.ZEN_DURATION_PROMPT import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.EnableZenModeDialog -import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription @@ -187,7 +187,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + zenModeRepository.activateMode(MANUAL_DND) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -2) collectLastValue(underTest.lockScreenState) runCurrent() @@ -233,7 +233,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER) collectLastValue(underTest.lockScreenState) runCurrent() @@ -278,7 +277,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { fun onTriggered_dndModeIsOff_settingNotFOREVERorPROMPT_dndWithDuration() = testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -900) runCurrent() @@ -323,7 +321,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { fun onTriggered_dndModeIsOff_settingIsPROMPT_showDialog() = testScope.runTest { val expandable: Expandable = mock() - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT) whenever(enableZenModeDialog.createDialog()).thenReturn(mock()) collectLastValue(underTest.lockScreenState) @@ -405,10 +402,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val lockScreenState by collectLastValue(underTest.lockScreenState) - val manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE - zenModeRepository.addMode(manualDnd) - runCurrent() - assertThat(lockScreenState) .isEqualTo( KeyguardQuickAffordanceConfig.LockScreenState.Visible( @@ -420,7 +413,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { ) ) - zenModeRepository.activateMode(manualDnd) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() assertThat(lockScreenState) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt index a8e390c25a4d..46d98f979655 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt @@ -23,11 +23,16 @@ import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest +import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.panels.ui.viewmodel.toolbar.editModeButtonViewModelFactory import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest @@ -36,6 +41,15 @@ class EditModeButtonViewModelTest : SysuiTestCase() { val underTest = kosmos.editModeButtonViewModelFactory.create() + @Before + fun setUp() { + with(kosmos) { + whenever(activityStarter.postQSRunnableDismissingKeyguard(any())).doAnswer { + (it.getArgument(0) as Runnable).run() + } + } + } + @Test fun falsingFalseTap_editModeDoesntStart() = kosmos.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index 04e094f25f5d..c8b3aba9b846 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -24,6 +24,7 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable @@ -87,9 +88,9 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val activeModes by collectLastValue(zenModeInteractor.activeModes) + zenModeRepository.activateMode(MANUAL_DND) zenModeRepository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder().setName("Mode 1").setActive(true).build(), TestModeBuilder().setName("Mode 2").setActive(true).build(), ) @@ -111,7 +112,7 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(zenModeInteractor.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + zenModeRepository.activateMode(MANUAL_DND) assertThat(dndMode?.isActive).isTrue() underTest.handleInput( @@ -127,7 +128,6 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(zenModeInteractor.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) assertThat(dndMode?.isActive).isFalse() underTest.handleInput( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 48edded5df18..de54e75281aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -575,4 +575,50 @@ class SceneInteractorTest : SysuiTestCase() { assertThat(currentScene).isNotEqualTo(disabledScene) } + + @Test + fun transitionAnimations() = + kosmos.runTest { + val isVisible by collectLastValue(underTest.isVisible) + assertThat(isVisible).isTrue() + + underTest.setVisible(false, "test") + assertThat(isVisible).isFalse() + + underTest.onTransitionAnimationStart() + // One animation is active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + + underTest.onTransitionAnimationStart() + // One animation is active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationCancelled() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + + underTest.setVisible(true, "test") + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationStart() + underTest.onTransitionAnimationStart() + // Two animations are active, forced visible. + assertThat(isVisible).isTrue() + + underTest.setVisible(false, "test") + // Two animations are active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // One animation is still active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 06dd046564df..51f056aa18da 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -36,6 +36,8 @@ import com.android.keyguard.AuthInteractionProperties import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.domain.interactor.authenticationInteractor @@ -141,6 +143,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever @SmallTest @@ -169,6 +172,7 @@ class SceneContainerStartableTest : SysuiTestCase() { private val uiEventLoggerFake = kosmos.uiEventLoggerFake private val msdlPlayer = kosmos.fakeMSDLPlayer private val authInteractionProperties = AuthInteractionProperties() + private val mockActivityTransitionAnimator = mock<ActivityTransitionAnimator>() private lateinit var underTest: SceneContainerStartable @@ -177,6 +181,8 @@ class SceneContainerStartableTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) .thenReturn(true) + kosmos.activityTransitionAnimator = mockActivityTransitionAnimator + underTest = kosmos.sceneContainerStartable } @@ -2716,6 +2722,27 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentOverlays).isEmpty() } + @Test + fun hydrateActivityTransitionAnimationState() = + kosmos.runTest { + underTest.start() + + val isVisible by collectLastValue(sceneInteractor.isVisible) + assertThat(isVisible).isTrue() + + sceneInteractor.setVisible(false, "reason") + assertThat(isVisible).isFalse() + + val argumentCaptor = argumentCaptor<ActivityTransitionAnimator.Listener>() + verify(mockActivityTransitionAnimator).addListener(argumentCaptor.capture()) + + val listeners = argumentCaptor.allValues + listeners.forEach { it.onTransitionAnimationStart() } + assertThat(isVisible).isTrue() + listeners.forEach { it.onTransitionAnimationEnd() } + assertThat(isVisible).isFalse() + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt index a98d1a2ea4a5..d3ba3dceb4cf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt @@ -22,10 +22,13 @@ import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.scene.ui.view.mockShadeRootView import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository import com.android.systemui.testKosmos +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -41,6 +44,7 @@ import org.mockito.kotlin.whenever class ShadeDisplaysInteractorTest : SysuiTestCase() { val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val testScope = kosmos.testScope private val shadeRootview = kosmos.mockShadeRootView private val positionRepository = kosmos.fakeShadeDisplaysRepository private val shadeContext = kosmos.mockedWindowContext @@ -49,7 +53,7 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { private val configuration = mock<Configuration>() private val display = mock<Display>() - private val underTest = kosmos.shadeDisplaysInteractor + private val underTest by lazy { kosmos.shadeDisplaysInteractor } @Before fun setup() { @@ -84,12 +88,14 @@ class ShadeDisplaysInteractorTest : SysuiTestCase() { } @Test - fun start_shadeInWrongPosition_logsStartToLatencyTracker() { - whenever(display.displayId).thenReturn(0) - positionRepository.setDisplayId(1) + fun start_shadeInWrongPosition_logsStartToLatencyTracker() = + testScope.runTest { + whenever(display.displayId).thenReturn(0) + positionRepository.setDisplayId(1) - underTest.start() + underTest.start() + advanceUntilIdle() - verify(latencyTracker).onShadeDisplayChanging(eq(1)) - } + verify(latencyTracker).onShadeDisplayChanging(eq(1)) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt new file mode 100644 index 000000000000..58396e7cef82 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractorTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 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.shade.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl.NotificationElement +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl.QSElement +import com.android.systemui.shade.shadeTestUtil +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +@EnableSceneContainer +class ShadeExpandedStateInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val testScope = kosmos.testScope + private val shadeTestUtil by lazy { kosmos.shadeTestUtil } + + private val underTest: ShadeExpandedStateInteractor by lazy { + kosmos.shadeExpandedStateInteractor + } + + @Test + fun expandedElement_qsExpanded_returnsQSElement() = + testScope.runTest { + shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion = 1f) + val currentlyExpandedElement = underTest.currentlyExpandedElement + + val element = currentlyExpandedElement.value + + assertThat(element).isInstanceOf(QSElement::class.java) + } + + @Test + fun expandedElement_shadeExpanded_returnsShade() = + testScope.runTest { + shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 1f, qsExpansion = 0f) + + val element = underTest.currentlyExpandedElement.value + + assertThat(element).isInstanceOf(NotificationElement::class.java) + } + + @Test + fun expandedElement_noneExpanded_returnsNull() = + testScope.runTest { + shadeTestUtil.setShadeAndQsExpansion(shadeExpansion = 0f, qsExpansion = 0f) + + val element = underTest.currentlyExpandedElement.value + + assertThat(element).isNull() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index 0061c4142e8b..75d000b63d62 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -22,7 +22,6 @@ import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue @@ -126,65 +125,8 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_positiveStartTime_notifIconFlagOff_iconIsPhone() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, notificationIcon = createStatusBarIconViewOrNull()) - ) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) - val icon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.SingleColorIcon) - .impl as Icon.Resource - assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) - assertThat(icon.contentDescription).isNotNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun chip_positiveStartTime_notifIconFlagOn_iconIsNotifIcon() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - val notifIcon = createStatusBarIconViewOrNull() - repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = notifIcon)) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java) - val actualIcon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.StatusBarView) - .impl - assertThat(actualIcon).isEqualTo(notifIcon) - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun chip_positiveStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel( - startTimeMs = 1000, - notificationIcon = createStatusBarIconViewOrNull(), - notificationKey = "notifKey", - ) - ) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey")) - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun chip_positiveStartTime_notifIconAndConnectedDisplaysFlagOn_iconIsNotifIcon() = + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_positiveStartTime_connectedDisplaysFlagOn_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -205,29 +147,8 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_zeroStartTime_notifIconFlagOff_iconIsPhone() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 0, notificationIcon = createStatusBarIconViewOrNull()) - ) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) - val icon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.SingleColorIcon) - .impl as Icon.Resource - assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) - assertThat(icon.contentDescription).isNotNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun chip_zeroStartTime_notifIconFlagOn_cdFlagOff_iconIsNotifIcon() = + fun chip_zeroStartTime_cdFlagOff_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -244,8 +165,8 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun chip_zeroStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() = + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_zeroStartTime_cdFlagOn_iconIsNotifKeyIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -262,7 +183,6 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOff_iconIsPhone() = testScope.runTest { @@ -281,7 +201,7 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_notifIconFlagOn_butNullNotifIcon_iconNotifKey() = testScope.runTest { val latest by collectLastValue(underTest.chip) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index e561e3ea27d7..902db5e10589 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -501,7 +501,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_hasHeadsUpByUser_onlyShowsIcon() = + fun chips_hasHeadsUpBySystem_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -523,7 +523,99 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) ) - // WHEN there's a HUN pinned by a user + // WHEN there's a HUN pinned by the system + kosmos.headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "notif", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + // THEN the chip keeps showing time + // (In real life the chip won't show at all, but that's handled in a different part of + // the system. What we know here is that the chip shouldn't shrink to icon only.) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 6543L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + val otherPromotedContentBuilder = + PromotedNotificationContentModel.Builder("other notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 654321L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + val icon = createStatusBarIconViewOrNull() + val otherIcon = createStatusBarIconViewOrNull() + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = icon, + promotedContent = promotedContentBuilder.build(), + ), + activeNotificationModel( + key = "other notif", + statusBarChipIcon = otherIcon, + promotedContent = otherPromotedContentBuilder.build(), + ), + ) + ) + + // WHEN there's a HUN pinned for the "other notif" chip + kosmos.headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "other notif", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + // THEN the "notif" chip keeps showing time + val chip = latest!![0] + assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java) + assertIsNotifChip(chip, icon, "notif") + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 6543L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = mock<StatusBarIconView>(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + // WHEN this notification is pinned by the user kosmos.headsUpNotificationRepository.setNotifications( UnconfinedFakeHeadsUpRowRepository( key = "notif", @@ -531,6 +623,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) ) + // THEN the chip shrinks to icon only assertThat(latest!![0]) .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index a3ffd91b1728..609885d0214b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -458,7 +458,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_hasNotifEntry_shownAsHUN() = + fun onPromotedNotificationChipTapped_hasNotifEntry_shownAsHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) @@ -473,7 +473,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_noNotifEntry_noHUN() = + fun onPromotedNotificationChipTapped_noNotifEntry_noHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(null) @@ -488,7 +488,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_shownAsHUNEvenIfEntryShouldNot() = + fun onPromotedNotificationChipTapped_shownAsHUNEvenIfEntryShouldNot() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) @@ -511,7 +511,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_atSameTimeAsOnAdded_promotedShownAsHUN() = + fun onPromotedNotificationChipTapped_atSameTimeAsOnAdded_promotedShownAsHUN() = testScope.runTest { // First, the promoted notification appears as not heads up val promotedEntry = NotificationEntryBuilder().setPkg("promotedPackage").build() @@ -548,6 +548,33 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun onPromotedNotificationChipTapped_chipTappedTwice_hunHiddenOnSecondTap() = + testScope.runTest { + whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) + + // WHEN chip tapped first + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + // THEN HUN is shown + finishBind(entry) + verify(headsUpManager).showNotification(entry, isPinnedByUser = true) + addHUN(entry) + + // WHEN chip is tapped again + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + // THEN HUN is hidden + verify(headsUpManager).removeNotification(eq(entry.key), eq(false), any()) + } + + @Test fun testTransferIsolatedChildAlert_withGroupAlertSummary() { setShouldHeadsUp(groupSummary) whenever(notifPipeline.allNotifs).thenReturn(listOf(groupSummary, groupSibling1)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt index 22ef408e266c..fae7d515d305 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -412,46 +413,53 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { @Test fun statusBarHeadsUpState_pinnedBySystem() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedBySystem) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedBySystem) + assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedBySystem)) + assertThat(status).isEqualTo(PinnedStatus.PinnedBySystem) } @Test fun statusBarHeadsUpState_pinnedByUser() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedByUser) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedByUser) + assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedByUser)) + assertThat(status).isEqualTo(PinnedStatus.PinnedByUser) } @Test fun statusBarHeadsUpState_withoutPinnedNotifications_notPinned() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", PinnedStatus.NotPinned) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.NotPinned) + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status).isEqualTo(PinnedStatus.NotPinned) } @Test fun statusBarHeadsUpState_whenShadeExpanded_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -463,13 +471,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // should emit `false`. kosmos.fakeShadeRepository.setLegacyShadeExpansion(1.0f) - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_notificationsAreHidden_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -477,13 +487,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND the notifications are hidden keyguardViewStateRepository.areNotificationsFullyHidden.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_onLockScreen_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -494,13 +506,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { testSetup = true, ) - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_onByPassLockScreen_true() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -513,13 +527,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND bypass is enabled faceAuthRepository.isBypassEnabled.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isTrue() + assertThat(state).isInstanceOf(TopPinnedState.Pinned::class.java) + assertThat(status!!.isPinned).isTrue() } @Test fun statusBarHeadsUpState_onByPassLockScreen_withoutNotifications_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN no pinned rows // AND the lock screen is shown @@ -530,7 +546,8 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND bypass is enabled faceAuthRepository.isBypassEnabled.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 9eafcdbadfa5..e2330f448a1b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.hardware.biometrics.BiometricSourceType; +import android.hardware.fingerprint.FingerprintManager; import android.os.Handler; import android.os.PowerManager; import android.os.UserHandle; @@ -431,9 +432,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test - public void onUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() { - // GIVEN UDFPS is supported - when(mUpdateMonitor.isUdfpsSupported()).thenReturn(true); + public void onOpticalUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() { + // GIVEN optical UDFPS is supported + when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(true); // WHEN udfps fails once - then don't show the bouncer yet mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); @@ -451,6 +452,25 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test + public void onUltrasonicUdfpsLockout_showPrimaryBouncer() { + // GIVEN ultrasonic UDFPS is supported + when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(false); + + // WHEN udfps fails three times, don't show bouncer + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); + + // WHEN lockout is received + mBiometricUnlockController.onBiometricError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "Lockout", BiometricSourceType.FINGERPRINT); + + // THEN show bouncer + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true); + } + + @Test public void onFinishedGoingToSleep_authenticatesWhenPending() { when(mUpdateMonitor.isGoingToSleep()).thenReturn(true); mBiometricUnlockController.mWakefulnessObserver.onFinishedGoingToSleep(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt index cf512cdee800..b98409906f8d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt @@ -28,9 +28,7 @@ import android.view.View import android.widget.LinearLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP import com.android.systemui.SysuiTestCase import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.dump.DumpManager @@ -42,7 +40,6 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -76,9 +73,8 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @OptIn(ExperimentalCoroutinesApi::class) -@EnableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP) @DisableFlags(StatusBarChipsModernization.FLAG_NAME) -class OngoingCallControllerViaRepoTest : SysuiTestCase() { +class OngoingCallControllerTest : SysuiTestCase() { private val kosmos = Kosmos() private val clock = kosmos.fakeSystemClock @@ -114,7 +110,6 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { testScope.backgroundScope, context, ongoingCallRepository, - mock<CommonNotifCollection>(), kosmos.activeNotificationsInteractor, clock, mockActivityStarter, @@ -162,28 +157,7 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun interactorHasOngoingCallNotif_notifIconFlagOff_repoHasNoNotifIcon() = - testScope.runTest { - val icon = mock<StatusBarIconView>() - setNotifOnRepo( - activeNotificationModel( - key = "ongoingNotif", - callType = CallType.Ongoing, - uid = CALL_UID, - statusBarChipIcon = icon, - whenTime = 567, - ) - ) - - val repoState = ongoingCallRepository.ongoingCallState.value - assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java) - assertThat((repoState as OngoingCallModel.InCall).notificationIconView).isNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun interactorHasOngoingCallNotif_notifIconFlagOn_repoHasNotifIcon() = + fun interactorHasOngoingCallNotif_repoHasNotifIcon() = testScope.runTest { val icon = mock<StatusBarIconView>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt deleted file mode 100644 index cd3539d6b9a5..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt +++ /dev/null @@ -1,694 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone.ongoingcall - -import android.app.ActivityManager -import android.app.IActivityManager -import android.app.IUidObserver -import android.app.Notification -import android.app.PendingIntent -import android.app.Person -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags -import android.service.notification.NotificationListenerService.REASON_USER_STOPPED -import android.testing.TestableLooper -import android.view.LayoutInflater -import android.view.View -import android.widget.LinearLayout -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP -import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.log.logcatLogBuffer -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.res.R -import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository -import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor -import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel -import com.android.systemui.statusbar.window.StatusBarWindowController -import com.android.systemui.statusbar.window.StatusBarWindowControllerStore -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.ArgumentMatchers.anyString -import org.mockito.ArgumentMatchers.nullable -import org.mockito.Mock -import org.mockito.Mockito.eq -import org.mockito.Mockito.mock -import org.mockito.Mockito.never -import org.mockito.Mockito.reset -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.whenever - -private const val CALL_UID = 900 - -// A process state that represents the process being visible to the user. -private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP - -// A process state that represents the process being invisible to the user. -private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE - -@SmallTest -@RunWith(AndroidJUnit4::class) -@TestableLooper.RunWithLooper -@OptIn(ExperimentalCoroutinesApi::class) -@DisableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, StatusBarChipsModernization.FLAG_NAME) -class OngoingCallControllerViaListenerTest : SysuiTestCase() { - private val kosmos = Kosmos() - - private val clock = FakeSystemClock() - private val mainExecutor = FakeExecutor(clock) - private val testScope = TestScope() - private val statusBarModeRepository = FakeStatusBarModeRepository() - private val ongoingCallRepository = kosmos.ongoingCallRepository - - private lateinit var controller: OngoingCallController - private lateinit var notifCollectionListener: NotifCollectionListener - - @Mock - private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler - @Mock private lateinit var mockOngoingCallListener: OngoingCallListener - @Mock private lateinit var mockActivityStarter: ActivityStarter - @Mock private lateinit var mockIActivityManager: IActivityManager - @Mock private lateinit var mockStatusBarWindowController: StatusBarWindowController - @Mock private lateinit var mockStatusBarWindowControllerStore: StatusBarWindowControllerStore - - private lateinit var chipView: View - - @Before - fun setUp() { - allowTestableLooperAsMainThread() - TestableLooper.get(this).runWithLooper { - chipView = - LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null) - } - - MockitoAnnotations.initMocks(this) - val notificationCollection = mock(CommonNotifCollection::class.java) - whenever(mockStatusBarWindowControllerStore.defaultDisplay) - .thenReturn(mockStatusBarWindowController) - - controller = - OngoingCallController( - testScope.backgroundScope, - context, - ongoingCallRepository, - notificationCollection, - kosmos.activeNotificationsInteractor, - clock, - mockActivityStarter, - mainExecutor, - mockIActivityManager, - DumpManager(), - mockStatusBarWindowControllerStore, - mockSwipeStatusBarAwayGestureHandler, - statusBarModeRepository, - logcatLogBuffer("OngoingCallControllerViaListenerTest"), - ) - controller.start() - controller.addCallback(mockOngoingCallListener) - controller.setChipView(chipView) - onTeardown { controller.tearDownChipView() } - - val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) - verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) - notifCollectionListener = collectionListenerCaptor.value!! - - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - } - - @Test - fun onEntryUpdated_isOngoingCallNotif_listenerAndRepoNotified() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(567) - notifCollectionListener.onEntryUpdated(notification.build()) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - val repoState = ongoingCallRepository.ongoingCallState.value - assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java) - assertThat((repoState as OngoingCallModel.InCall).startTimeMs).isEqualTo(567) - } - - @Test - fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true) - } - - @Test - fun onEntryUpdated_notOngoingCallNotif_listenerAndRepoNotNotified() { - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_listenerNotifiedTwice() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - verify(mockOngoingCallListener, times(2)).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_repoUpdated() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - /** Regression test for b/191472854. */ - @Test - fun onEntryUpdated_notifHasNullContentIntent_noCrash() { - notifCollectionListener.onEntryUpdated( - createCallNotifEntry(ongoingCallStyle, nullContentIntent = true) - ) - } - - /** Regression test for b/192379214. */ - @Test - @DisableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME, FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsZero_timeHidden() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(0) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isEqualTo(0) - } - - @Test - @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME) - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsZero_timeShown() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(0) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsValid_timeShown() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(clock.currentTimeMillis()) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) - } - - /** Regression test for b/194731244. */ - @Test - fun onEntryUpdated_calledManyTimes_uidObserverOnlyRegisteredOnce() { - for (i in 0 until 4) { - // Re-create the notification each time so that it's considered a different object and - // will re-trigger the whole flow. - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - verify(mockIActivityManager, times(1)).registerUidObserver(any(), any(), any(), any()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_getUidProcessStateThrowsException_noCrash() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenThrow(SecurityException()) - - // No assert required, just check no crash - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_registerUidObserverThrowsException_noCrash() { - `when`( - mockIActivityManager.registerUidObserver( - any(), - any(), - any(), - nullable(String::class.java), - ) - ) - .thenThrow(SecurityException()) - - // No assert required, just check no crash - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_packageNameProvidedToActivityManager() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - val packageNameCaptor = ArgumentCaptor.forClass(String::class.java) - verify(mockIActivityManager) - .registerUidObserver(any(), any(), any(), packageNameCaptor.capture()) - assertThat(packageNameCaptor.value).isNotNull() - } - - /** - * If a call notification is never added before #onEntryRemoved is called, then the listener - * should never be notified. - */ - @Test - fun onEntryRemoved_callNotifNeverAddedBeforehand_listenerNotNotified() { - notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_callNotifAddedThenRemoved_listenerNotified() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockOngoingCallListener) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_callNotifAddedThenRemoved_repoUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryUpdated_callNotifAddedThenRemoved_windowControllerUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false) - } - - /** Regression test for b/188491504. */ - @Test - fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_listenerNotified() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockOngoingCallListener) - - // Create another notification based on the ongoing call one, but remove the features that - // made it a call notification. - val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry) - removedEntryBuilder.modifyNotification(context).style = null - - notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - /** Regression test for b/188491504. */ - @Test - fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_repoUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - - // Create another notification based on the ongoing call one, but remove the features that - // made it a call notification. - val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry) - removedEntryBuilder.modifyNotification(context).style = null - - notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_listenerNotNotified() { - notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_repoNotUpdated() { - notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry()) - - notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - } - - @Test - fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() { - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_unrelatedNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_screeningCallNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isTrue() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_VISIBLE) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() { - val invalidChipView = LinearLayout(context) - controller.setChipView(invalidChipView) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenRemoved_returnsFalse() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - - notifCollectionListener.onEntryUpdated(ongoingCallNotifEntry) - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenScreeningCallNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenUnrelatedNotifSent_returnsTrue() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isTrue() - } - - /** - * This test fakes a theme change during an ongoing call. - * - * When a theme change happens, [CollapsedStatusBarFragment] and its views get re-created, so - * [OngoingCallController.setChipView] gets called with a new view. If there's an active ongoing - * call when the theme changes, the new view needs to be updated with the call information. - */ - @Test - fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() { - // Start an ongoing call. - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - lateinit var newChipView: View - TestableLooper.get(this).runWithLooper { - newChipView = - LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null) - } - - // Change the chip view associated with the controller. - controller.setChipView(newChipView) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun callProcessChangesToVisible_listenerNotified() { - // Start the call while the process is invisible. - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) - verify(mockIActivityManager) - .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java)) - val uidObserver = captor.value - - // Update the process to visible. - uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0) - mainExecutor.advanceClockToLast() - mainExecutor.runAllReady() - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun callProcessChangesToInvisible_listenerNotified() { - // Start the call while the process is visible. - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_VISIBLE) - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) - verify(mockIActivityManager) - .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java)) - val uidObserver = captor.value - - // Update the process to invisible. - uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0) - mainExecutor.advanceClockToLast() - mainExecutor.runAllReady() - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - /** Regression test for b/212467440. */ - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() { - val notifEntry = createOngoingCallNotifEntry() - val pendingIntent = notifEntry.sbn.notification.contentIntent - notifCollectionListener.onEntryUpdated(notifEntry) - - chipView.performClick() - - // Ensure that the sysui didn't modify the notification's intent -- see b/212467440. - verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any()) - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun callNotificationAdded_chipIsClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(chipView.hasOnClickListeners()).isTrue() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun callNotificationAdded_newChipsEnabled_chipNotClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(chipView.hasOnClickListeners()).isFalse() - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun fullscreenIsTrue_chipStillClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - assertThat(chipView.hasOnClickListeners()).isTrue() - } - - // Swipe gesture tests - - @Test - fun callStartedInImmersiveMode_swipeGestureCallbackAdded() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockSwipeStatusBarAwayGestureHandler) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false - testScope.runCurrent() - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockSwipeStatusBarAwayGestureHandler, never()) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun transitionToImmersiveMode_swipeGestureCallbackAdded() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - verify(mockSwipeStatusBarAwayGestureHandler) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockSwipeStatusBarAwayGestureHandler) - - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false - testScope.runCurrent() - - verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString()) - } - - @Test - fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockSwipeStatusBarAwayGestureHandler) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString()) - } - - // TODO(b/195839150): Add test - // swipeGesturedTriggeredPreviously_entersImmersiveModeAgain_callbackNotAdded(). That's - // difficult to add now because we have no way to trigger [SwipeStatusBarAwayGestureHandler]'s - // callbacks in test. - - // END swipe gesture tests - - private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle) - - private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle) - - private fun createCallNotifEntry( - callStyle: Notification.CallStyle, - nullContentIntent: Boolean = false, - ): NotificationEntry { - val notificationEntryBuilder = NotificationEntryBuilder() - notificationEntryBuilder.modifyNotification(context).style = callStyle - notificationEntryBuilder.setUid(CALL_UID) - - if (nullContentIntent) { - notificationEntryBuilder.modifyNotification(context).setContentIntent(null) - } else { - val contentIntent = mock(PendingIntent::class.java) - notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent) - } - - return notificationEntryBuilder.build() - } - - private fun createNotCallNotifEntry() = NotificationEntryBuilder().build() -} - -private val person = Person.Builder().setName("name").build() -private val hangUpIntent = mock(PendingIntent::class.java) - -private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent) -private val screeningCallStyle = - Notification.CallStyle.forScreeningCall( - person, - hangUpIntent, - /* answerIntent= */ mock(PendingIntent::class.java), - ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index c6bae197ad76..7802b921eae0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt @@ -33,6 +33,7 @@ import androidx.test.filters.SmallTest import com.android.internal.R import com.android.settingslib.notification.data.repository.updateNotificationPolicy import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -204,7 +205,7 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun shouldAskForZenDuration_changesWithSetting() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE + val manualDnd = TestModeBuilder().makeManualDnd().setActive(true).build() settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() @@ -233,29 +234,27 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun activateMode_usesCorrectDuration() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE - zenModeRepository.addModes(listOf(manualDnd)) settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() - underTest.activateMode(manualDnd) - assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)).isNull() + underTest.activateMode(MANUAL_DND) + assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id)).isNull() - zenModeRepository.deactivateMode(manualDnd.id) + zenModeRepository.deactivateMode(MANUAL_DND) settingsRepository.setInt(ZEN_DURATION, 60) runCurrent() - underTest.activateMode(manualDnd) - assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)) + underTest.activateMode(MANUAL_DND) + assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id)) .isEqualTo(Duration.ofMinutes(60)) } @Test fun deactivateAllModes_updatesCorrectModes() = testScope.runTest { + zenModeRepository.activateMode(MANUAL_DND) zenModeRepository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder().setName("Inactive").setActive(false).build(), TestModeBuilder().setName("Active").setActive(true).build(), ) @@ -389,12 +388,9 @@ class ZenModeInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(underTest.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) - runCurrent() - assertThat(dndMode!!.isActive).isFalse() - zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND_INACTIVE.id) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() assertThat(dndMode!!.isActive).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt index 07d088bbb3d6..856de8ee1c80 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -28,6 +28,7 @@ import android.service.notification.ZenModeConfig.ScheduleInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.settingslib.notification.modes.ZenMode import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -111,7 +112,6 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setName("Disabled by other") .setEnabled(false, /* byUser= */ false) .build(), - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder() .setName("Enabled") .setEnabled(true) @@ -128,14 +128,14 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(tiles).hasSize(3) with(tiles?.elementAt(0)!!) { - assertThat(this.text).isEqualTo("Disabled by other") - assertThat(this.subtext).isEqualTo("Not set") + assertThat(this.text).isEqualTo("Do Not Disturb") + assertThat(this.subtext).isEqualTo("Off") assertThat(this.enabled).isEqualTo(false) } with(tiles?.elementAt(1)!!) { - assertThat(this.text).isEqualTo("Do Not Disturb") - assertThat(this.subtext).isEqualTo("On") - assertThat(this.enabled).isEqualTo(true) + assertThat(this.text).isEqualTo("Disabled by other") + assertThat(this.subtext).isEqualTo("Not set") + assertThat(this.enabled).isEqualTo(false) } with(tiles?.elementAt(2)!!) { assertThat(this.text).isEqualTo("Enabled") @@ -176,18 +176,24 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Do Not Disturb") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) } - with(tiles?.elementAt(1)!!) { + with(tiles?.elementAt(2)!!) { assertThat(this.text).isEqualTo("Active with manual") assertThat(this.subtext).isEqualTo("On • trigger description") assertThat(this.enabled).isEqualTo(true) } - with(tiles?.elementAt(2)!!) { + with(tiles?.elementAt(3)!!) { assertThat(this.text).isEqualTo("Inactive with manual") assertThat(this.subtext).isEqualTo("Off") assertThat(this.enabled).isEqualTo(false) @@ -226,10 +232,11 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) // Check that tile is initially present - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) @@ -239,8 +246,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { runCurrent() } // Check that tile is still present at the same location, but turned off - assertThat(tiles).hasSize(3) - with(tiles?.elementAt(0)!!) { + assertThat(tiles).hasSize(4) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -252,9 +259,9 @@ class ModesDialogViewModelTest : SysuiTestCase() { runCurrent() // Check that tile is now gone - assertThat(tiles2).hasSize(2) - assertThat(tiles2?.elementAt(0)!!.text).isEqualTo("Active with manual") - assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Inactive with manual") + assertThat(tiles2).hasSize(3) + assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Active with manual") + assertThat(tiles2?.elementAt(2)!!.text).isEqualTo("Inactive with manual") } @Test @@ -287,22 +294,23 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) repository.removeMode("A") runCurrent() - assertThat(tiles).hasSize(2) + assertThat(tiles).hasSize(3) repository.removeMode("B") runCurrent() - assertThat(tiles).hasSize(1) + assertThat(tiles).hasSize(2) repository.removeMode("C") runCurrent() - assertThat(tiles).hasSize(0) + assertThat(tiles).hasSize(1) } @Test @@ -353,14 +361,15 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles!!).hasSize(7) - assertThat(tiles!![0].subtext).isEqualTo("When the going gets tough") - assertThat(tiles!![1].subtext).isEqualTo("On • When in Rome") - assertThat(tiles!![2].subtext).isEqualTo("Not set") - assertThat(tiles!![3].subtext).isEqualTo("Off") - assertThat(tiles!![4].subtext).isEqualTo("On") - assertThat(tiles!![5].subtext).isEqualTo("Not set") - assertThat(tiles!![6].subtext).isEqualTo(timeScheduleMode.triggerDescription) + // Manual DND is included by default + assertThat(tiles!!).hasSize(8) + assertThat(tiles!![1].subtext).isEqualTo("When the going gets tough") + assertThat(tiles!![2].subtext).isEqualTo("On • When in Rome") + assertThat(tiles!![3].subtext).isEqualTo("Not set") + assertThat(tiles!![4].subtext).isEqualTo("Off") + assertThat(tiles!![5].subtext).isEqualTo("On") + assertThat(tiles!![6].subtext).isEqualTo("Not set") + assertThat(tiles!![7].subtext).isEqualTo(timeScheduleMode.triggerDescription) } @Test @@ -411,32 +420,33 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles!!).hasSize(7) - with(tiles?.elementAt(0)!!) { + // Manual DND is included by default + assertThat(tiles!!).hasSize(8) + with(tiles?.elementAt(1)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("When the going gets tough") } - with(tiles?.elementAt(1)!!) { + with(tiles?.elementAt(2)!!) { assertThat(this.stateDescription).isEqualTo("On") assertThat(this.subtextDescription).isEqualTo("When in Rome") } - with(tiles?.elementAt(2)!!) { + with(tiles?.elementAt(3)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("Not set") } - with(tiles?.elementAt(3)!!) { + with(tiles?.elementAt(4)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEmpty() } - with(tiles?.elementAt(4)!!) { + with(tiles?.elementAt(5)!!) { assertThat(this.stateDescription).isEqualTo("On") assertThat(this.subtextDescription).isEmpty() } - with(tiles?.elementAt(5)!!) { + with(tiles?.elementAt(6)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("Not set") } - with(tiles?.elementAt(6)!!) { + with(tiles?.elementAt(7)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription) .isEqualTo( @@ -456,31 +466,30 @@ class ModesDialogViewModelTest : SysuiTestCase() { val tiles by collectLastValue(underTest.tiles) val modeId = "id" - repository.addModes( - listOf( - TestModeBuilder() - .setId(modeId) - .setName("Test") - .setManualInvocationAllowed(true) - .build() - ) + repository.addMode( + TestModeBuilder() + .setId(modeId) + .setName("Test") + .setManualInvocationAllowed(true) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) - assertThat(tiles?.elementAt(0)?.enabled).isFalse() + // Manual DND is included by default + assertThat(tiles).hasSize(2) + assertThat(tiles?.elementAt(1)?.enabled).isFalse() // Trigger onClick - tiles?.first()?.onClick?.let { it() } + tiles?.elementAt(1)?.onClick?.let { it() } runCurrent() - assertThat(tiles?.first()?.enabled).isTrue() + assertThat(tiles?.elementAt(1)?.enabled).isTrue() // Trigger onClick - tiles?.first()?.onClick?.let { it() } + tiles?.elementAt(1)?.onClick?.let { it() } runCurrent() - assertThat(tiles?.first()?.enabled).isFalse() + assertThat(tiles?.elementAt(1)?.enabled).isFalse() } @Test @@ -489,25 +498,24 @@ class ModesDialogViewModelTest : SysuiTestCase() { val job = Job() val tiles by collectLastValue(underTest.tiles, context = job) - repository.addModes( - listOf( - TestModeBuilder() - .setName("Active without manual") - .setActive(true) - .setManualInvocationAllowed(false) - .build() - ) + repository.addMode( + TestModeBuilder() + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) + // Manual DND is included by default + assertThat(tiles).hasSize(2) // Click tile to toggle it off - tiles?.elementAt(0)!!.onClick() + tiles?.elementAt(1)!!.onClick() runCurrent() - assertThat(tiles).hasSize(1) - with(tiles?.elementAt(0)!!) { + assertThat(tiles).hasSize(2) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -518,7 +526,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { } // Check that nothing happened - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -530,19 +538,18 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) - repository.addModes( - listOf( - TestModeBuilder() - .setId("ID") - .setName("Disabled by other") - .setEnabled(false, /* byUser= */ false) - .build() - ) + repository.addMode( + TestModeBuilder() + .setId("ID") + .setName("Disabled by other") + .setEnabled(false, /* byUser= */ false) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) - with(tiles?.elementAt(0)!!) { + // Manual DND is included by default + assertThat(tiles).hasSize(2) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Disabled by other") assertThat(this.subtext).isEqualTo("Not set") assertThat(this.enabled).isEqualTo(false) @@ -561,7 +568,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { .isEqualTo("ID") // Check that nothing happened to the tile - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Disabled by other") assertThat(this.subtext).isEqualTo("Not set") assertThat(this.enabled).isEqualTo(false) @@ -593,10 +600,11 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(2) + // Manual DND is included by default + assertThat(tiles).hasSize(3) // Trigger onLongClick for A - tiles?.first()?.onLongClick?.let { it() } + tiles?.elementAt(1)?.onLongClick?.let { it() } runCurrent() // Check that it launched the correct intent @@ -625,9 +633,9 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) + repository.activateMode(MANUAL_DND) repository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder() .setId("id1") .setName("Inactive Mode One") @@ -644,6 +652,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() + // Manual DND is included by default assertThat(tiles).hasSize(3) // Trigger onClick for each tile in sequence @@ -672,19 +681,17 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) - repository.addModes( - listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, - TestModeBuilder() - .setId("id1") - .setName("Inactive Mode One") - .setActive(false) - .setManualInvocationAllowed(true) - .build(), - ) + repository.addMode( + TestModeBuilder() + .setId("id1") + .setName("Inactive Mode One") + .setActive(false) + .setManualInvocationAllowed(true) + .build() ) runCurrent() + // Manual DND is included by default assertThat(tiles).hasSize(2) val modeCaptor = argumentCaptor<ZenMode>() diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt index d84d89087349..812a964bf8bc 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt @@ -30,10 +30,6 @@ data class ClockConfig( /** Transition to AOD should move smartspace like large clock instead of small clock */ val useAlternateSmartspaceAODTransition: Boolean = false, - /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */ - @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone") - val isReactiveToTone: Boolean = true, - /** True if the clock is large frame clock, which will use weather in compose. */ val useCustomClockScene: Boolean = false, ) diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 58f2d3ccc6a8..67f620f6fc54 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -19,6 +19,7 @@ android:id="@+id/volume_dialog_root" android:layout_width="match_parent" android:layout_height="match_parent" + android:alpha="0" android:clipChildren="false" app:layoutDescription="@xml/volume_dialog_scene"> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d445ed927a25..80fb8b9fcebc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2309,6 +2309,9 @@ <string name="group_system_lock_screen">Lock screen</string> <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] --> <string name="group_system_quick_memo">Take a note</string> + <!-- TODO(b/383734125): make it translatable once string is finalized by UXW.--> + <!-- User visible title for the keyboard shortcut that toggles Voice Access. [CHAR LIMIT=70] --> + <string name="group_system_toggle_voice_access" translatable="false">Toggle Voice Access</string> <!-- User visible title for the multitasking keyboard shortcuts list. [CHAR LIMIT=70] --> <string name="keyboard_shortcut_group_system_multitasking">Multitasking</string> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index a703b027b691..7d291c311ca3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -2591,6 +2591,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * @return true if optical udfps HW is supported on this device. Can return true even if the + * user has not enrolled udfps. This may be false if called before + * onAllAuthenticatorsRegistered. + */ + public boolean isOpticalUdfpsSupported() { + return mAuthController.isOpticalUdfpsSupported(); + } + + /** * @return true if there's at least one sfps enrollment for the current user. */ public boolean isSfpsEnrolled() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java index 04afd8693e04..caf043a1b1be 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java @@ -22,6 +22,8 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.annotation.Nullable; import android.annotation.UiContext; import android.content.ComponentCallbacks; import android.content.Context; @@ -44,7 +46,8 @@ import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; -import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import androidx.annotation.NonNull; @@ -57,12 +60,16 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; import com.android.systemui.util.leak.RotationUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.concurrent.Executor; import java.util.function.Supplier; public class FullscreenMagnificationController implements ComponentCallbacks { - private static final String TAG = "FullscreenMagnificationController"; + private static final String TAG = "FullscreenMagController"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final Context mContext; private final AccessibilityManager mAccessibilityManager; private final WindowManager mWindowManager; @@ -77,12 +84,14 @@ public class FullscreenMagnificationController implements ComponentCallbacks { private int mBorderStoke; private final int mDisplayId; private static final Region sEmptyRegion = new Region(); - private ValueAnimator mShowHideBorderAnimator; + @VisibleForTesting + @Nullable + ValueAnimator mShowHideBorderAnimator; private Handler mHandler; private Executor mExecutor; - private boolean mFullscreenMagnificationActivated = false; private final Configuration mConfiguration; - private final Runnable mShowBorderRunnable = this::showBorderWithNullCheck; + private final Runnable mHideBorderImmediatelyRunnable = this::hideBorderImmediately; + private final Runnable mShowBorderRunnable = this::showBorder; private int mRotation; private final IRotationWatcher mRotationWatcher = new IRotationWatcher.Stub() { @Override @@ -95,6 +104,21 @@ public class FullscreenMagnificationController implements ComponentCallbacks { private final DisplayManager.DisplayListener mDisplayListener; private String mCurrentDisplayUniqueId; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DISABLED, + DISABLING, + ENABLING, + ENABLED + }) + @interface FullscreenMagnificationActivationState {} + private static final int DISABLED = 0; + private static final int DISABLING = 1; + private static final int ENABLING = 2; + private static final int ENABLED = 3; + @FullscreenMagnificationActivationState + private int mActivationState = DISABLED; + public FullscreenMagnificationController( @UiContext Context context, @Main Handler handler, @@ -106,7 +130,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks { Supplier<SurfaceControlViewHost> scvhSupplier) { this(context, handler, executor, displayManager, accessibilityManager, windowManager, iWindowManager, scvhSupplier, - new SurfaceControl.Transaction(), null); + new SurfaceControl.Transaction()); } @VisibleForTesting @@ -119,8 +143,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks { WindowManager windowManager, IWindowManager iWindowManager, Supplier<SurfaceControlViewHost> scvhSupplier, - SurfaceControl.Transaction transaction, - ValueAnimator valueAnimator) { + SurfaceControl.Transaction transaction) { mContext = context; mHandler = handler; mExecutor = executor; @@ -135,18 +158,6 @@ public class FullscreenMagnificationController implements ComponentCallbacks { mConfiguration = new Configuration(context.getResources().getConfiguration()); mLongAnimationTimeMs = mContext.getResources().getInteger( com.android.internal.R.integer.config_longAnimTime); - mShowHideBorderAnimator = (valueAnimator == null) - ? createNullTargetObjectAnimator() : valueAnimator; - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { - if (isReverse) { - // The animation was played in reverse, which means we are hiding the border. - // We would like to perform clean up after the border is fully hidden. - cleanUpBorder(); - } - } - }); mCurrentDisplayUniqueId = mContext.getDisplayNoVerify().getUniqueId(); mDisplayManager = displayManager; mDisplayListener = new DisplayManager.DisplayListener() { @@ -167,20 +178,51 @@ public class FullscreenMagnificationController implements ComponentCallbacks { // Same unique ID means the physical display doesn't change. Early return. return; } - mCurrentDisplayUniqueId = uniqueId; - applyCornerRadiusToBorder(); + mHandler.post(FullscreenMagnificationController.this::applyCornerRadiusToBorder); } }; } - private ValueAnimator createNullTargetObjectAnimator() { + @VisibleForTesting + @UiThread + ValueAnimator createShowTargetAnimator(@NonNull View target) { + if (mShowHideBorderAnimator != null) { + mShowHideBorderAnimator.cancel(); + } + final ValueAnimator valueAnimator = - ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f); - Interpolator interpolator = new AccelerateDecelerateInterpolator(); + ObjectAnimator.ofFloat(target, View.ALPHA, 0f, 1f); + Interpolator interpolator = new AccelerateInterpolator(); valueAnimator.setInterpolator(interpolator); valueAnimator.setDuration(mLongAnimationTimeMs); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation) { + mHandler.post(() -> setState(ENABLED)); + }}); + return valueAnimator; + } + + @VisibleForTesting + @UiThread + ValueAnimator createHideTargetAnimator(@NonNull View target) { + if (mShowHideBorderAnimator != null) { + mShowHideBorderAnimator.cancel(); + } + + final ValueAnimator valueAnimator = + ObjectAnimator.ofFloat(target, View.ALPHA, 1f, 0f); + Interpolator interpolator = new DecelerateInterpolator(); + + valueAnimator.setInterpolator(interpolator); + valueAnimator.setDuration(mLongAnimationTimeMs); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation) { + mHandler.post(() -> cleanUpBorder()); + }}); return valueAnimator; } @@ -190,14 +232,10 @@ public class FullscreenMagnificationController implements ComponentCallbacks { */ @UiThread public void onFullscreenMagnificationActivationChanged(boolean activated) { - final boolean changed = (mFullscreenMagnificationActivated != activated); - if (changed) { - mFullscreenMagnificationActivated = activated; - if (activated) { - createFullscreenMagnificationBorder(); - } else { - removeFullscreenMagnificationBorder(); - } + if (activated) { + createFullscreenMagnificationBorder(); + } else { + removeFullscreenMagnificationBorder(); } } @@ -207,16 +245,21 @@ public class FullscreenMagnificationController implements ComponentCallbacks { */ @UiThread private void removeFullscreenMagnificationBorder() { - if (mHandler.hasCallbacks(mShowBorderRunnable)) { - mHandler.removeCallbacks(mShowBorderRunnable); + int state = getState(); + if (state == DISABLING || state == DISABLED) { + // If there is an ongoing disable process or it is already disabled, return + return; } - mContext.unregisterComponentCallbacks(this); - - - mShowHideBorderAnimator.reverse(); + setState(DISABLING); + mShowHideBorderAnimator = createHideTargetAnimator(mFullscreenBorder); + mShowHideBorderAnimator.start(); } - private void cleanUpBorder() { + @VisibleForTesting + @UiThread + void cleanUpBorder() { + mContext.unregisterComponentCallbacks(this); + if (Flags.updateCornerRadiusOnDisplayChanged()) { mDisplayManager.unregisterDisplayListener(mDisplayListener); } @@ -227,6 +270,12 @@ public class FullscreenMagnificationController implements ComponentCallbacks { } if (mFullscreenBorder != null) { + if (mHandler.hasCallbacks(mHideBorderImmediatelyRunnable)) { + mHandler.removeCallbacks(mHideBorderImmediatelyRunnable); + } + if (mHandler.hasCallbacks(mShowBorderRunnable)) { + mHandler.removeCallbacks(mShowBorderRunnable); + } mFullscreenBorder = null; try { mIWindowManager.removeRotationWatcher(mRotationWatcher); @@ -234,6 +283,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks { Log.w(TAG, "Failed to remove rotation watcher", e); } } + setState(DISABLED); } /** @@ -242,44 +292,47 @@ public class FullscreenMagnificationController implements ComponentCallbacks { */ @UiThread private void createFullscreenMagnificationBorder() { + int state = getState(); + if (state == ENABLING || state == ENABLED) { + // If there is an ongoing enable process or it is already enabled, return + return; + } + if (mShowHideBorderAnimator != null) { + mShowHideBorderAnimator.cancel(); + } + setState(ENABLING); + onConfigurationChanged(mContext.getResources().getConfiguration()); mContext.registerComponentCallbacks(this); if (mSurfaceControlViewHost == null) { - // Create the view only if it does not exist yet. If we are trying to enable fullscreen - // magnification before it was fully disabled, we use the previous view instead of - // creating a new one. + // Create the view only if it does not exist yet. If we are trying to enable + // fullscreen magnification before it was fully disabled, we use the previous view + // instead of creating a new one. mFullscreenBorder = LayoutInflater.from(mContext) .inflate(R.layout.fullscreen_magnification_border, null); - // Set the initial border view alpha manually so we won't show the border accidentally - // after we apply show() to the SurfaceControl and before the animation starts to run. + // Set the initial border view alpha manually so we won't show the border + // accidentally after we apply show() to the SurfaceControl and before the + // animation starts to run. mFullscreenBorder.setAlpha(0f); - mShowHideBorderAnimator.setTarget(mFullscreenBorder); mSurfaceControlViewHost = mScvhSupplier.get(); mSurfaceControlViewHost.setView(mFullscreenBorder, getBorderLayoutParams()); - mBorderSurfaceControl = mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(); + mBorderSurfaceControl = + mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(); try { mIWindowManager.watchRotation(mRotationWatcher, Display.DEFAULT_DISPLAY); } catch (Exception e) { Log.w(TAG, "Failed to register rotation watcher", e); } if (Flags.updateCornerRadiusOnDisplayChanged()) { - mHandler.post(this::applyCornerRadiusToBorder); + applyCornerRadiusToBorder(); } } mTransaction .addTransactionCommittedListener( mExecutor, - () -> { - if (mShowHideBorderAnimator.isRunning()) { - // Since the method is only called when there is an activation - // status change, the running animator is hiding the border. - mShowHideBorderAnimator.reverse(); - } else { - mShowHideBorderAnimator.start(); - } - }) + this::showBorder) .setPosition(mBorderSurfaceControl, -mBorderOffset, -mBorderOffset) .setLayer(mBorderSurfaceControl, Integer.MAX_VALUE) .show(mBorderSurfaceControl) @@ -380,19 +433,25 @@ public class FullscreenMagnificationController implements ComponentCallbacks { mHandler.removeCallbacks(mShowBorderRunnable); } - // We hide the border immediately as early as possible to beat the redrawing of window - // in response to the orientation change so users won't see a weird shape border. - mHandler.postAtFrontOfQueue(() -> { - mFullscreenBorder.setAlpha(0f); - }); - + // We hide the border immediately as early as possible to beat the redrawing of + // window in response to the orientation change so users won't see a weird shape + // border. + mHandler.postAtFrontOfQueue(mHideBorderImmediatelyRunnable); mHandler.postDelayed(mShowBorderRunnable, mLongAnimationTimeMs); } - private void showBorderWithNullCheck() { + @UiThread + private void hideBorderImmediately() { if (mShowHideBorderAnimator != null) { - mShowHideBorderAnimator.start(); + mShowHideBorderAnimator.cancel(); } + mFullscreenBorder.setAlpha(0f); + } + + @UiThread + private void showBorder() { + mShowHideBorderAnimator = createShowTargetAnimator(mFullscreenBorder); + mShowHideBorderAnimator.start(); } private void updateDimensions() { @@ -404,7 +463,9 @@ public class FullscreenMagnificationController implements ComponentCallbacks { R.dimen.magnifier_border_width_fullscreen_with_offset); } - private void applyCornerRadiusToBorder() { + @UiThread + @VisibleForTesting + void applyCornerRadiusToBorder() { if (!isActivated()) { return; } @@ -422,6 +483,20 @@ public class FullscreenMagnificationController implements ComponentCallbacks { backgroundDrawable.setCornerRadius(cornerRadius); } + @UiThread + private void setState(@FullscreenMagnificationActivationState int state) { + if (DEBUG) { + Log.d(TAG, "setState from " + mActivationState + " to " + state); + } + mActivationState = state; + } + + @VisibleForTesting + @UiThread + int getState() { + return mActivationState; + } + @Override public void onLowMemory() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index 5f0acfa644dc..67aa4ff577b8 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.content.Context; import android.hardware.display.DisplayManager; import android.os.Handler; -import android.os.UserHandle; import android.text.TextUtils; import android.view.Display; import android.view.WindowManager; @@ -58,7 +57,7 @@ public class AccessibilityFloatingMenuController implements private final AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private Context mContext; + private final Context mContext; private final WindowManager mWindowManager; private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final DisplayManager mDisplayManager; @@ -226,7 +225,6 @@ public class AccessibilityFloatingMenuController implements @Override public void onUserInitializationComplete(int userId) { mIsUserInInitialization = false; - mContext = mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 0); mBtnMode = mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode(); mBtnTargets = mAccessibilityButtonTargetsObserver.getCurrentAccessibilityButtonTargets(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java index 121b51f768e7..a1cb0367421b 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java @@ -79,6 +79,8 @@ class MenuInfoRepository { private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED; private final Context mContext; + // Pref always get the userId from the context to store SharedPreferences for the correct user + private final Context mCurrentUserContext; private final Configuration mConfiguration; private final AccessibilityManager mAccessibilityManager; private final AccessibilityManager.AccessibilityServicesStateChangeListener @@ -157,6 +159,9 @@ class MenuInfoRepository { OnContentsChanged settingsContentsChanged, SecureSettings secureSettings, @Nullable HearingAidDeviceManager hearingAidDeviceManager) { mContext = context; + final int currentUserId = secureSettings.getRealUserHandle(UserHandle.USER_CURRENT); + mCurrentUserContext = context.createContextAsUser( + UserHandle.of(currentUserId), /* flags= */ 0); mAccessibilityManager = accessibilityManager; mConfiguration = new Configuration(context.getResources().getConfiguration()); mSettingsContentsCallback = settingsContentsChanged; @@ -168,12 +173,13 @@ class MenuInfoRepository { void loadMenuMoveToTucked(OnInfoReady<Boolean> callback) { callback.onReady( - Prefs.getBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, + Prefs.getBoolean( + mCurrentUserContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, DEFAULT_MOVE_TO_TUCKED_VALUE)); } void loadDockTooltipVisibility(OnInfoReady<Boolean> callback) { - callback.onReady(Prefs.getBoolean(mContext, + callback.onReady(Prefs.getBoolean(mCurrentUserContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE)); } @@ -215,19 +221,19 @@ class MenuInfoRepository { } void updateMoveToTucked(boolean isMoveToTucked) { - Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, + Prefs.putBoolean(mCurrentUserContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, isMoveToTucked); } void updateMenuSavingPosition(Position percentagePosition) { mPercentagePosition = percentagePosition; - Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, + Prefs.putString(mCurrentUserContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, percentagePosition.toString()); } void updateDockTooltipVisibility(boolean hasSeen) { - Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, - hasSeen); + Prefs.putBoolean(mCurrentUserContext, + Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, hasSeen); } void updateMigrationTooltipVisibility(boolean visible) { @@ -243,7 +249,7 @@ class MenuInfoRepository { } private Position getStartPosition() { - final String absolutePositionString = Prefs.getString(mContext, + final String absolutePositionString = Prefs.getString(mCurrentUserContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); final float defaultPositionXPercent = diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index 184518ac35eb..e7470a34a065 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -17,6 +17,7 @@ package com.android.systemui.accessibility.floatingmenu; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import android.content.Context; import android.graphics.PixelFormat; @@ -90,7 +91,8 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); params.receiveInsetsIgnoringZOrder = true; - params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; + params.privateFlags |= + PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION | SYSTEM_FLAG_SHOW_FOR_ALL_USERS; params.windowAnimations = android.R.style.Animation_Translucent; // Insets are configured to allow the menu to display over navigation and system bars. params.setFitInsetsTypes(0); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index d0cb507789a1..eee5f9e34317 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -1011,6 +1011,16 @@ public class AuthController implements } /** + * @return true if optical udfps HW is supported on this device. Can return true even if + * the user has not enrolled udfps. This may be false if called before + * onAllAuthenticatorsRegistered. + */ + public boolean isOpticalUdfpsSupported() { + return getUdfpsProps() != null && !getUdfpsProps().isEmpty() && getUdfpsProps() + .get(0).isOpticalUdfps(); + } + + /** * @return true if ultrasonic udfps HW is supported on this device. Can return true even if * the user has not enrolled udfps. This may be false if called before * onAllAuthenticatorsRegistered. diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt index 78156dbc8964..ca49de3b1510 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt @@ -25,6 +25,7 @@ import com.android.compose.animation.scene.TransitionKey */ object CommunalTransitionKeys { /** Fades the glanceable hub without any translation */ + @Deprecated("No longer supported as all hub transitions will be fades.") val SimpleFade = TransitionKey("SimpleFade") /** Transition from the glanceable hub before entering edit mode */ val ToEditMode = TransitionKey("ToEditMode") diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index 41a59a959771..ae6238724042 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.util.kotlin.FlowDumperImpl @@ -50,6 +51,8 @@ class DeviceEntryHapticsInteractor constructor( biometricSettingsRepository: BiometricSettingsRepository, deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor, + deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, + keyguardBypassInteractor: KeyguardBypassInteractor, deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor: DeviceEntrySourceInteractor, fingerprintPropertyRepository: FingerprintPropertyRepository, @@ -82,7 +85,7 @@ constructor( emit(recentPowerButtonPressThresholdMs * -1L - 1L) } - val playSuccessHaptic: Flow<Unit> = + private val playHapticsOnDeviceEntry: Flow<Boolean> = deviceEntrySourceInteractor.deviceEntryFromBiometricSource .sample( combine( @@ -92,17 +95,29 @@ constructor( ::Triple, ) ) - .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> + .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> val sideFpsAllowsHaptic = !powerButtonDown && systemClock.uptimeMillis() - lastPowerButtonWakeup > recentPowerButtonPressThresholdMs val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic if (!allowHaptic) { - logger.d("Skip success haptic. Recent power button press or button is down.") + logger.d( + "Skip success entry haptic from power button. Recent power button press or button is down." + ) } allowHaptic } + + private val playHapticsOnFaceAuthSuccessAndBypassDisabled: Flow<Boolean> = + deviceEntryFaceAuthInteractor.isAuthenticated + .filter { it } + .sample(keyguardBypassInteractor.isBypassAvailable) + .map { !it } + + val playSuccessHaptic: Flow<Unit> = + merge(playHapticsOnDeviceEntry, playHapticsOnFaceAuthSuccessAndBypassDisabled) + .filter { it } // map to Unit .map {} .dumpWhileCollecting("playSuccessHaptic") diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 129a6bb72996..2ed0671a570b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -23,13 +23,7 @@ import com.android.server.notification.Flags.crossAppPoliteNotifications import com.android.server.notification.Flags.politeNotifications import com.android.server.notification.Flags.vibrateWhileUnlocked import com.android.systemui.Flags.FLAG_COMMUNAL_HUB -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON -import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP import com.android.systemui.Flags.communalHub -import com.android.systemui.Flags.statusBarCallChipNotificationIcon -import com.android.systemui.Flags.statusBarScreenSharingChips -import com.android.systemui.Flags.statusBarUseReposForCallChip import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade @@ -63,10 +57,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // DualShade dependencies DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag() - - // Status bar chip dependencies - statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken - statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken } private inline val politeNotifications @@ -86,17 +76,4 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha private inline val communalHub get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) - - private inline val statusBarCallChipNotificationIconToken - get() = - FlagToken( - FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, - statusBarCallChipNotificationIcon(), - ) - - private inline val statusBarScreenSharingChipsToken - get() = FlagToken(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, statusBarScreenSharingChips()) - - private inline val statusBarUseReposForCallChipToken - get() = FlagToken(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, statusBarUseReposForCallChip()) } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index e1ebf7cdf472..cf5c3402792e 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -463,6 +463,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mTelephonyListenerManager.removeServiceStateListener(mPhoneStateListener); mGlobalSettings.unregisterContentObserverSync(mAirplaneModeObserver); mConfigurationController.removeCallback(this); + if (mShowSilentToggle) { + mRingerModeTracker.getRingerMode().removeObservers(this); + } } protected Context getContext() { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt index e255bdea6100..6a42cdc876ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt @@ -41,6 +41,8 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVI import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS +import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System @@ -63,6 +65,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System, KEY_GESTURE_TYPE_ALL_APPS to System, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to System, // Multitasking Category KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to MultiTasking, @@ -100,6 +103,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.shortcut_helper_category_system_apps, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to R.string.shortcut_helper_category_system_apps, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.shortcut_helper_category_system_apps, // Multitasking Category KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to @@ -148,6 +152,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.group_system_access_google_assistant, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to R.string.group_system_access_google_assistant, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.group_system_toggle_voice_access, // Multitasking Category KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to R.string.group_system_cycle_forward, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt index 5060abdda247..c3c9df97a682 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt @@ -32,12 +32,14 @@ import android.view.KeyEvent.KEYCODE_RECENT_APPS import android.view.KeyEvent.KEYCODE_S import android.view.KeyEvent.KEYCODE_SLASH import android.view.KeyEvent.KEYCODE_TAB +import android.view.KeyEvent.KEYCODE_V import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CTRL_ON import android.view.KeyEvent.META_META_ON import android.view.KeyEvent.META_SHIFT_ON import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo +import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures import com.android.systemui.Flags.shortcutHelperKeyGlyph import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo @@ -118,8 +120,8 @@ constructor(@Main private val resources: Resources, private val inputManager: In return shortcuts } - private fun systemControlsShortcuts() = - listOf( + private fun systemControlsShortcuts(): List<KeyboardShortcutInfo> { + val shortcuts = mutableListOf( // Access list of all apps and search (i.e. Search/Launcher): // - Meta shortcutInfo(resources.getString(R.string.group_system_access_all_apps_search)) { @@ -176,6 +178,19 @@ constructor(@Main private val resources: Resources, private val inputManager: In }, ) + if (enableVoiceAccessKeyGestures()) { + shortcuts.add( + // Toggle voice access: + // - Meta + Alt + V + shortcutInfo(resources.getString(R.string.group_system_toggle_voice_access)) { + command(META_META_ON or META_ALT_ON, KEYCODE_V) + } + ) + } + + return shortcuts + } + private fun systemAppsShortcuts() = listOf( // Pull up Notes app for quick memo: diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt index f60621882ac0..59990ea22c2f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar import com.android.systemui.classifier.domain.interactor.FalsingInteractor +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel import dagger.assisted.AssistedFactory @@ -27,11 +28,12 @@ class EditModeButtonViewModel constructor( private val editModeViewModel: EditModeViewModel, private val falsingInteractor: FalsingInteractor, + private val activityStarter: ActivityStarter, ) { fun onButtonClick() { if (!falsingInteractor.isFalseTap(FalsingManager.LOW_PENALTY)) { - editModeViewModel.startEditing() + activityStarter.postQSRunnableDismissingKeyguard { editModeViewModel.startEditing() } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index c34edc81bfe7..30d1f05771d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -118,7 +118,8 @@ constructor( override fun addCallback(callback: QSTile.Callback?) { callback ?: return callbacks.add(callback) - state?.let(callback::onStateChanged) + state.copyTo(cachedState) + state.let(callback::onStateChanged) } override fun removeCallback(callback: QSTile.Callback?) { @@ -212,9 +213,9 @@ constructor( qsTileViewModel.destroy() } - override fun getState(): QSTile.State = + override fun getState(): QSTile.AdapterState = qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) } - ?: QSTile.State() + ?: QSTile.AdapterState() override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId @@ -241,7 +242,7 @@ constructor( context: Context, viewModelState: QSTileState, config: QSTileConfig, - ): QSTile.State = + ): QSTile.AdapterState = // we have to use QSTile.BooleanState to support different side icons // which are bound to instanceof QSTile.BooleanState in QSTileView. QSTile.AdapterState().apply { diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index d60f05e685bb..0488962fd076 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -90,22 +90,15 @@ constructor( initialValue = defaultTransitionState, ) - fun changeScene( - toScene: SceneKey, - transitionKey: TransitionKey? = null, - ) { - dataSource.changeScene( - toScene = toScene, - transitionKey = transitionKey, - ) + /** Number of currently active transition animations. */ + val activeTransitionAnimationCount = MutableStateFlow(0) + + fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) { + dataSource.changeScene(toScene = toScene, transitionKey = transitionKey) } - fun snapToScene( - toScene: SceneKey, - ) { - dataSource.snapToScene( - toScene = toScene, - ) + fun snapToScene(toScene: SceneKey) { + dataSource.snapToScene(toScene = toScene) } /** @@ -116,10 +109,7 @@ constructor( * [overlay] is already shown. */ fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.showOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + dataSource.showOverlay(overlay = overlay, transitionKey = transitionKey) } /** @@ -130,10 +120,7 @@ constructor( * if [overlay] is already hidden. */ fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.hideOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + dataSource.hideOverlay(overlay = overlay, transitionKey = transitionKey) } /** @@ -143,11 +130,7 @@ constructor( * This throws if [from] is not currently shown or if [to] is already shown. */ fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.replaceOverlay( - from = from, - to = to, - transitionKey = transitionKey, - ) + dataSource.replaceOverlay(from = from, to = to, transitionKey = transitionKey) } /** Sets whether the container is visible. */ diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 0e6fc36fb96a..ba9dc7625769 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update /** * Generic business logic and app state accessors for the scene framework. @@ -165,12 +166,15 @@ constructor( /** Whether the scene container is visible. */ val isVisible: StateFlow<Boolean> = - combine(repository.isVisible, repository.isRemoteUserInputOngoing) { - isVisible, - isRemoteUserInteractionOngoing -> + combine( + repository.isVisible, + repository.isRemoteUserInputOngoing, + repository.activeTransitionAnimationCount, + ) { isVisible, isRemoteUserInteractionOngoing, activeTransitionAnimationCount -> isVisibleInternal( raw = isVisible, isRemoteUserInputOngoing = isRemoteUserInteractionOngoing, + activeTransitionAnimationCount = activeTransitionAnimationCount, ) } .stateIn( @@ -436,8 +440,9 @@ constructor( private fun isVisibleInternal( raw: Boolean = repository.isVisible.value, isRemoteUserInputOngoing: Boolean = repository.isRemoteUserInputOngoing.value, + activeTransitionAnimationCount: Int = repository.activeTransitionAnimationCount.value, ): Boolean { - return raw || isRemoteUserInputOngoing + return raw || isRemoteUserInputOngoing || activeTransitionAnimationCount > 0 } /** @@ -525,4 +530,50 @@ constructor( ): Flow<Map<UserAction, UserActionResult>> { return disabledContentInteractor.filteredUserActions(unfiltered) } + + /** + * Notifies that a transition animation has started. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationStart() { + repository.activeTransitionAnimationCount.update { current -> + (current + 1).also { + check(it < 10) { + "Number of active transition animations is too high. Something must be" + + " calling onTransitionAnimationStart too many times!" + } + } + } + } + + /** + * Notifies that a transition animation has ended. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationEnd() { + decrementActiveTransitionAnimationCount() + } + + /** + * Notifies that a transition animation has been canceled. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationCancelled() { + decrementActiveTransitionAnimationCount() + } + + private fun decrementActiveTransitionAnimationCount() { + repository.activeTransitionAnimationCount.update { current -> + (current - 1).also { + check(it >= 0) { + "Number of active transition animations is negative. Something must be" + + " calling onTransitionAnimationEnd or onTransitionAnimationCancelled too" + + " many times!" + } + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 8d8c24eae9e2..3a23a71cf7bf 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -25,6 +25,7 @@ import com.android.internal.logging.UiEventLogger import com.android.keyguard.AuthInteractionProperties import com.android.systemui.CoreStartable import com.android.systemui.Flags +import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor @@ -146,6 +147,7 @@ constructor( private val vibratorHelper: VibratorHelper, private val msdlPlayer: MSDLPlayer, private val disabledContentInteractor: DisabledContentInteractor, + private val activityTransitionAnimator: ActivityTransitionAnimator, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() @@ -169,6 +171,7 @@ constructor( handleKeyguardEnabledness() notifyKeyguardDismissCancelledCallbacks() refreshLockscreenEnabled() + hydrateActivityTransitionAnimationState() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -929,6 +932,35 @@ constructor( } } + /** + * Wires the scene framework to activity transition animations that originate from anywhere. A + * subset of these may actually originate from UI inside one of the scenes in the framework. + * + * Telling the scene framework about ongoing activity transition animations is critical so the + * scene framework doesn't make its scene container invisible during a transition. + * + * As it turns out, making the scene container view invisible during a transition animation + * disrupts the animation and causes interaction jank CUJ tracking to ignore reports of the CUJ + * ending or being canceled. + */ + private fun hydrateActivityTransitionAnimationState() { + activityTransitionAnimator.addListener( + object : ActivityTransitionAnimator.Listener { + override fun onTransitionAnimationStart() { + sceneInteractor.onTransitionAnimationStart() + } + + override fun onTransitionAnimationEnd() { + sceneInteractor.onTransitionAnimationEnd() + } + + override fun onTransitionAnimationCancelled() { + sceneInteractor.onTransitionAnimationCancelled() + } + } + ) + } + companion object { private const val TAG = "SceneContainerStartable" } diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 42d83637ec1a..a48d4d4d3b5f 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -316,7 +316,7 @@ internal constructor( val callback = it.callback.get() if (callback != null) { it.executor.execute { - traceSection({ "$callback" }) { action(callback) { latch.countDown() } } + traceSection({ "UserTrackerImpl::$callback" }) { action(callback) { latch.countDown() } } } } else { latch.countDown() diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index c4306d3f7530..19bf4c0bab81 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -3959,7 +3959,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } final boolean isTrackpadThreeFingerSwipe = isTrackpadThreeFingerSwipe(event); - if (com.android.systemui.Flags.disableShadeExpandsOnTrackpadTwoFingerSwipe() + if (com.android.systemui.Flags.disableShadeTrackpadTwoFingerSwipe() && !isTrackpadThreeFingerSwipe && isTwoFingerSwipeTrackpadEvent(event) && !isPanelExpanded()) { if (isDown) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index 63e8ba8f65cd..747642097327 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -37,12 +37,13 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository -import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor -import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl import com.android.systemui.shade.display.ShadeDisplayPolicyModule +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeDisplaysInteractor +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.phone.ConfigurationControllerImpl import com.android.systemui.statusbar.phone.ConfigurationForwarder @@ -276,6 +277,8 @@ object ShadeDisplayAwareModule { @Module internal interface OptionalShadeDisplayAwareBindings { @BindsOptionalOf fun bindOptionalOfWindowRootView(): WindowRootView + + @BindsOptionalOf fun bindOptionalOShadeExpandedStateInteractor(): ShadeExpandedStateInteractor } /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 2348a110eb3a..b9df9f868dc3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -35,6 +35,8 @@ import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLega import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorSceneContainerImpl import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractorImpl +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl @@ -176,4 +178,10 @@ abstract class ShadeModule { @Binds @SysUISingleton abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorImpl): ShadeModeInteractor + + @Binds + @SysUISingleton + abstract fun bindShadeExpandedStateInteractor( + impl: ShadeExpandedStateInteractorImpl + ): ShadeExpandedStateInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt index 11805992fd6a..9a9fc467c53f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt @@ -27,7 +27,7 @@ import com.android.app.tracing.coroutines.TrackTracer * them across various threads' logs. */ object ShadeTraceLogger { - private val t = TrackTracer(trackName = "ShadeTraceLogger", trackGroup = "shade") + val t = TrackTracer(trackName = "ShadeTraceLogger", trackGroup = "shade") @JvmStatic fun logOnMovedToDisplay(displayId: Int, config: Configuration) { @@ -44,8 +44,11 @@ object ShadeTraceLogger { t.instant { "moveShadeWindowTo(displayId=$displayId)" } } - @JvmStatic - fun traceReparenting(r: () -> Unit) { + suspend fun traceReparenting(r: suspend () -> Unit) { t.traceAsync({ "reparenting" }) { r() } } + + inline fun traceWaitForExpansion(expansion: Float, r: () -> Unit) { + t.traceAsync({ "waiting for shade expansion to match $expansion" }) { r() } + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt new file mode 100644 index 000000000000..eab00166c8ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeExpandedStateInteractor.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 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.shade.domain.interactor + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** Fake [ShadeExpandedStateInteractor] for tests. */ +class FakeShadeExpandedStateInteractor : ShadeExpandedStateInteractor { + + private val mutableExpandedElement = + MutableStateFlow<ShadeExpandedStateInteractor.ShadeElement?>(null) + override val currentlyExpandedElement: StateFlow<ShadeExpandedStateInteractor.ShadeElement?> + get() = mutableExpandedElement + + fun setState(state: ShadeExpandedStateInteractor.ShadeElement?) { + mutableExpandedElement.value = state + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt index be561b178136..691a383cb338 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.shade.ShadeTraceLogger.traceReparenting import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.window.flags.Flags +import java.util.Optional import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope @@ -47,8 +48,19 @@ constructor( @Background private val bgScope: CoroutineScope, @Main private val mainThreadContext: CoroutineContext, private val shadeDisplayChangeLatencyTracker: ShadeDisplayChangeLatencyTracker, + shadeExpandedInteractor: Optional<ShadeExpandedStateInteractor>, ) : CoreStartable { + private val shadeExpandedInteractor = + shadeExpandedInteractor.orElse(null) + ?: error( + """ + ShadeExpandedStateInteractor must be provided for ShadeDisplaysInteractor to work. + If it is not, it means this is being instantiated in a SystemUI variant that shouldn't. + """ + .trimIndent() + ) + override fun start() { ShadeWindowGoesAround.isUnexpectedlyInLegacyMode() bgScope.launchTraced(TAG) { @@ -78,9 +90,12 @@ constructor( withContext(mainThreadContext) { traceReparenting { shadeDisplayChangeLatencyTracker.onShadeDisplayChanging(destinationId) + val expandedElement = shadeExpandedInteractor.currentlyExpandedElement.value + expandedElement?.collapse(reason = "Shade window move") reparentToDisplayId(id = destinationId) + expandedElement?.expand(reason = "Shade window move") + checkContextDisplayMatchesExpected(destinationId) } - checkContextDisplayMatchesExpected(destinationId) } } catch (e: IllegalStateException) { Log.e( diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt new file mode 100644 index 000000000000..dd3abeec5a72 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeExpandedStateInteractor.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2025 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.shade.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.ShadeTraceLogger.traceWaitForExpansion +import com.android.systemui.shade.domain.interactor.ShadeExpandedStateInteractor.ShadeElement +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.util.kotlin.Utils.Companion.combineState +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout + +/** + * Wrapper around [ShadeInteractor] to facilitate expansion and collapse of Notifications and quick + * settings. + * + * Specifially created to simplify [ShadeDisplaysInteractor] logic. + * + * NOTE: with [SceneContainerFlag] or [DualShade] disabled, [currentlyExpandedElement] will always + * return null! + */ +interface ShadeExpandedStateInteractor { + /** Returns the expanded [ShadeElement]. If none is, returns null. */ + val currentlyExpandedElement: StateFlow<ShadeElement?> + + /** An element from the shade window that can be expanded or collapsed. */ + abstract class ShadeElement { + /** Expands the shade element, returning when the expansion is done */ + abstract suspend fun expand(reason: String) + + /** Collapses the shade element, returning when the collapse is done. */ + abstract suspend fun collapse(reason: String) + } +} + +@SysUISingleton +class ShadeExpandedStateInteractorImpl +@Inject +constructor( + private val shadeInteractor: ShadeInteractor, + @Background private val bgScope: CoroutineScope, +) : ShadeExpandedStateInteractor { + + private val notificationElement = NotificationElement() + private val qsElement = QSElement() + + override val currentlyExpandedElement: StateFlow<ShadeElement?> = + if (SceneContainerFlag.isEnabled) { + combineState( + shadeInteractor.isShadeAnyExpanded, + shadeInteractor.isQsExpanded, + bgScope, + SharingStarted.Eagerly, + ) { isShadeAnyExpanded, isQsExpanded -> + when { + isShadeAnyExpanded -> notificationElement + isQsExpanded -> qsElement + else -> null + } + } + } else { + MutableStateFlow(null) + } + + inner class NotificationElement : ShadeElement() { + override suspend fun expand(reason: String) { + shadeInteractor.expandNotificationsShade(reason) + shadeInteractor.shadeExpansion.waitUntil(1f) + } + + override suspend fun collapse(reason: String) { + shadeInteractor.collapseNotificationsShade(reason) + shadeInteractor.shadeExpansion.waitUntil(0f) + } + } + + inner class QSElement : ShadeElement() { + override suspend fun expand(reason: String) { + shadeInteractor.expandQuickSettingsShade(reason) + shadeInteractor.qsExpansion.waitUntil(1f) + } + + override suspend fun collapse(reason: String) { + shadeInteractor.collapseQuickSettingsShade(reason) + shadeInteractor.qsExpansion.waitUntil(0f) + } + } + + private suspend fun StateFlow<Float>.waitUntil(f: Float) { + // it's important to not do this in the main thread otherwise it will block any rendering. + withContext(bgScope.coroutineContext) { + withTimeout(1.seconds) { traceWaitForExpansion(expansion = f) { first { it == f } } } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index de08e3891902..86954d569199 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel import android.view.View import com.android.internal.jank.InteractionJankMonitor -import com.android.systemui.Flags import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon @@ -64,18 +63,12 @@ constructor( is OngoingCallModel.InCallWithVisibleApp -> OngoingActivityChipModel.Hidden() is OngoingCallModel.InCall -> { val icon = - if ( - Flags.statusBarCallChipNotificationIcon() && - state.notificationIconView != null - ) { + if (state.notificationIconView != null) { StatusBarConnectedDisplays.assertInLegacyMode() OngoingActivityChipModel.ChipIcon.StatusBarView( state.notificationIconView ) - } else if ( - StatusBarConnectedDisplays.isEnabled && - Flags.statusBarCallChipNotificationIcon() - ) { + } else if (StatusBarConnectedDisplays.isEnabled) { OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon( state.notificationKey ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 2f6431b05c8b..ec3a5b271e35 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import javax.inject.Inject @@ -60,7 +61,7 @@ constructor( /** Converts the notification to the [OngoingActivityChipModel] object. */ private fun NotificationChipModel.toActivityChipModel( - headsUpState: PinnedStatus + headsUpState: TopPinnedState ): OngoingActivityChipModel.Shown { StatusBarNotifChips.assertInNewMode() val icon = @@ -87,8 +88,12 @@ constructor( } } - if (headsUpState == PinnedStatus.PinnedByUser) { - // If the user tapped the chip to show the HUN, we want to just show the icon because + val isShowingHeadsUpFromChipTap = + headsUpState is TopPinnedState.Pinned && + headsUpState.status == PinnedStatus.PinnedByUser && + headsUpState.key == this.key + if (isShowingHeadsUpFromChipTap) { + // If the user tapped this chip to show the HUN, we want to just show the icon because // the HUN will show the rest of the information. return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt new file mode 100644 index 000000000000..1be5842bceeb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2024 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.chips.ui.compose + +import android.content.res.ColorStateList +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.compose.modifiers.neverDecreaseWidth +import com.android.systemui.statusbar.chips.ui.model.ColorsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel + +@Composable +fun OngoingActivityChip(model: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) { + val context = LocalContext.current + val isClickable = model.onClickListener != null + val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView + + // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible + // height of the chip is determined by the height of the background of the Row below. + Box( + contentAlignment = Alignment.Center, + modifier = + modifier + .fillMaxHeight() + .clickable( + enabled = isClickable, + onClick = { + // TODO(b/372657935): Implement click actions. + }, + ), + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.clip( + RoundedCornerShape( + dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius) + ) + ) + .height(dimensionResource(R.dimen.ongoing_appops_chip_height)) + .widthIn( + min = + if (isClickable) { + dimensionResource(id = R.dimen.min_clickable_item_size) + } else { + 0.dp + } + ) + .background(Color(model.colors.background(context).defaultColor)) + .padding( + horizontal = + if (hasEmbeddedIcon) { + 0.dp + } else { + dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) + } + ), + ) { + model.icon?.let { ChipIcon(viewModel = it, colors = model.colors) } + + val isIconOnly = model is OngoingActivityChipModel.Shown.IconOnly + val isTextOnly = model.icon == null + if (!isIconOnly) { + ChipContent( + viewModel = model, + modifier = + Modifier.padding( + start = + if (isTextOnly || hasEmbeddedIcon) { + 0.dp + } else { + dimensionResource( + id = R.dimen.ongoing_activity_chip_icon_text_padding + ) + }, + end = + if (hasEmbeddedIcon) { + dimensionResource( + id = + R.dimen + .ongoing_activity_chip_text_end_padding_for_embedded_padding_icon + ) + } else { + 0.dp + }, + ), + ) + } + } + } +} + +@Composable +private fun ChipIcon( + viewModel: OngoingActivityChipModel.ChipIcon, + colors: ColorsModel, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + when (viewModel) { + is OngoingActivityChipModel.ChipIcon.StatusBarView -> { + val originalIcon = viewModel.impl + val iconSizePx = + context.resources.getDimensionPixelSize( + R.dimen.ongoing_activity_chip_embedded_padding_icon_size + ) + AndroidView( + modifier = modifier, + factory = { _ -> + originalIcon.apply { + layoutParams = ViewGroup.LayoutParams(iconSizePx, iconSizePx) + imageTintList = ColorStateList.valueOf(colors.text(context)) + } + }, + ) + } + + is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> { + Icon( + icon = viewModel.impl, + tint = Color(colors.text(context)), + modifier = + modifier.size(dimensionResource(id = R.dimen.ongoing_activity_chip_icon_size)), + ) + } + + // TODO(b/372657935): Add recommended architecture implementation for + // StatusBarNotificationIcons + is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> {} + } +} + +@Composable +private fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) { + val context = LocalContext.current + when (viewModel) { + is OngoingActivityChipModel.Shown.Timer -> { + ChronometerText( + startTimeMillis = viewModel.startTimeMs, + style = MaterialTheme.typography.labelLarge, + color = Color(viewModel.colors.text(context)), + modifier = modifier, + ) + } + + is OngoingActivityChipModel.Shown.Countdown -> { + ChipText( + text = viewModel.secondsUntilStarted.toString(), + color = Color(viewModel.colors.text(context)), + style = MaterialTheme.typography.labelLarge, + modifier = modifier.neverDecreaseWidth(), + backgroundColor = Color(viewModel.colors.background(context).defaultColor), + ) + } + + is OngoingActivityChipModel.Shown.Text -> { + ChipText( + text = viewModel.text, + color = Color(viewModel.colors.text(context)), + style = MaterialTheme.typography.labelLarge, + modifier = modifier, + backgroundColor = Color(viewModel.colors.background(context).defaultColor), + ) + } + + is OngoingActivityChipModel.Shown.ShortTimeDelta -> { + // TODO(b/372657935): Implement ShortTimeDelta content in compose. + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt new file mode 100644 index 000000000000..85ea087f531b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 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.chips.ui.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel + +@Composable +fun OngoingActivityChips(chips: MultipleOngoingActivityChipsModel, modifier: Modifier = Modifier) { + Row( + // TODO(b/372657935): Remove magic numbers for padding and spacing. + modifier = modifier.fillMaxHeight().padding(horizontal = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + // TODO(b/372657935): Make sure chips are only shown when there is enough horizontal + // space. + if (chips.primary is OngoingActivityChipModel.Shown) { + OngoingActivityChip(model = chips.primary) + } + if (chips.secondary is OngoingActivityChipModel.Shown) { + OngoingActivityChip(model = chips.secondary) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index c81e8e211507..956d99e46766 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.chips.ui.model import android.view.View -import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips @@ -130,10 +129,6 @@ sealed class OngoingActivityChipModel { */ data class StatusBarView(val impl: StatusBarIconView) : ChipIcon { init { - check(Flags.statusBarCallChipNotificationIcon()) { - "OngoingActivityChipModel.ChipIcon.StatusBarView created even though " + - "Flags.statusBarCallChipNotificationIcon is not enabled" - } StatusBarConnectedDisplays.assertInLegacyMode() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index c7535ec14d5d..eb5a3703bcfb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -112,20 +112,20 @@ constructor( if (StatusBarNotifChips.isEnabled) { applicationScope.launch { statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent.collect { - showPromotedNotificationHeadsUp(it) + onPromotedNotificationChipTapEvent(it) } } } } /** - * Shows the promoted notification with the given [key] as heads-up. + * Updates the heads-up state based on which promoted notification with the given [key] was + * tapped. * * Must be run on the main thread. */ - private fun showPromotedNotificationHeadsUp(key: String) { + private fun onPromotedNotificationChipTapEvent(key: String) { StatusBarNotifChips.assertInNewMode() - mLogger.logShowPromotedNotificationHeadsUp(key) val entry = notifCollection.getEntry(key) if (entry == null) { @@ -135,22 +135,29 @@ constructor( // TODO(b/364653005): Validate that the given key indeed matches a promoted notification, // not just any notification. + val isCurrentlyHeadsUp = mHeadsUpManager.isHeadsUpEntry(entry.key) val posted = PostedEntry( entry, wasAdded = false, wasUpdated = false, - // Force-set this notification to show heads-up. - shouldHeadsUpEver = true, - shouldHeadsUpAgain = true, + // We want the chip to act as a toggle, so if the chip's notification is currently + // showing as heads up, then we should stop showing it. + shouldHeadsUpEver = !isCurrentlyHeadsUp, + shouldHeadsUpAgain = !isCurrentlyHeadsUp, isPinnedByUser = true, - isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key), + isHeadsUpEntry = isCurrentlyHeadsUp, isBinding = isEntryBinding(entry), ) + if (isCurrentlyHeadsUp) { + mLogger.logHidePromotedNotificationHeadsUp(key) + } else { + mLogger.logShowPromotedNotificationHeadsUp(key) + } mExecutor.execute { mPostedEntries[entry.key] = posted - mNotifPromoter.invalidateList("showPromotedNotificationHeadsUp: ${entry.logKey}") + mNotifPromoter.invalidateList("onPromotedNotificationChipTapEvent: ${entry.logKey}") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt index e443a0418ffd..5141aa35b041 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -148,6 +148,15 @@ class HeadsUpCoordinatorLogger(private val buffer: LogBuffer, private val verbos ) } + fun logHidePromotedNotificationHeadsUp(key: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "requesting promoted entry to hide heads up: $str1" }, + ) + } + fun logPromotedNotificationForHeadsUpNotFound(key: String) { buffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index 75c7d2d5be98..6140c92369b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -25,6 +25,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey import javax.inject.Inject @@ -98,21 +99,39 @@ constructor( } } - /** What [PinnedStatus] does the top row have? */ - private val topPinnedStatus: Flow<PinnedStatus> = + /** What [PinnedStatus] and key does the top row have? */ + private val topPinnedState: Flow<TopPinnedState> = headsUpRepository.activeHeadsUpRows.flatMapLatest { rows -> if (rows.isNotEmpty()) { - combine(rows.map { it.pinnedStatus }) { pinnedStatus -> - pinnedStatus.firstOrNull { it.isPinned } ?: PinnedStatus.NotPinned + // For each row, emits a (key, pinnedStatus) pair each time any row's + // `pinnedStatus` changes + val toCombine: List<Flow<Pair<String, PinnedStatus>>> = + rows.map { row -> row.pinnedStatus.map { status -> row.key to status } } + combine(toCombine) { pairs -> + val topPinnedRow: Pair<String, PinnedStatus>? = + pairs.firstOrNull { it.second.isPinned } + if (topPinnedRow != null) { + TopPinnedState.Pinned( + key = topPinnedRow.first, + status = topPinnedRow.second, + ) + } else { + TopPinnedState.NothingPinned + } } } else { - // if the set is empty, there are no flows to combine - flowOf(PinnedStatus.NotPinned) + flowOf(TopPinnedState.NothingPinned) } } /** Are there any pinned heads up rows to display? */ - val hasPinnedRows: Flow<Boolean> = topPinnedStatus.map { it.isPinned } + val hasPinnedRows: Flow<Boolean> = + topPinnedState.map { + when (it) { + is TopPinnedState.Pinned -> it.status.isPinned + is TopPinnedState.NothingPinned -> false + } + } val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { @@ -142,13 +161,25 @@ constructor( } } - /** Emits the pinned notification state as it relates to the status bar. */ - val statusBarHeadsUpState: Flow<PinnedStatus> = - combine(topPinnedStatus, canShowHeadsUp) { topPinnedStatus, canShowHeadsUp -> + /** + * Emits the pinned notification state as it relates to the status bar. Includes both the pinned + * status and key of the notification that's pinned (if there is a pinned notification). + */ + val statusBarHeadsUpState: Flow<TopPinnedState> = + combine(topPinnedState, canShowHeadsUp) { topPinnedState, canShowHeadsUp -> if (canShowHeadsUp) { - topPinnedStatus + topPinnedState } else { - PinnedStatus.NotPinned + TopPinnedState.NothingPinned + } + } + + /** Emits the pinned notification status as it relates to the status bar. */ + val statusBarHeadsUpStatus: Flow<PinnedStatus> = + statusBarHeadsUpState.map { + when (it) { + is TopPinnedState.Pinned -> it.status + is TopPinnedState.NothingPinned -> PinnedStatus.NotPinned } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt index 042389f7fde7..fd5973e0ab3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt @@ -25,7 +25,6 @@ import android.graphics.drawable.Icon import android.service.notification.StatusBarNotification import android.util.ArrayMap import com.android.app.tracing.traceSection -import com.android.systemui.Flags import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry @@ -132,12 +131,6 @@ private class ActiveNotificationsStoreBuilder( } private fun NotificationEntry.toModel(): ActiveNotificationModel { - val statusBarChipIcon = - if (Flags.statusBarCallChipNotificationIcon()) { - icons.statusBarChipIcon - } else { - null - } val promotedContent = if (PromotedNotificationContentModel.featureFlagEnabled()) { promotedNotificationContentModel @@ -158,7 +151,7 @@ private class ActiveNotificationsStoreBuilder( aodIcon = icons.aodIcon?.sourceIcon, shelfIcon = icons.shelfIcon?.sourceIcon, statusBarIcon = icons.statusBarIcon?.sourceIcon, - statusBarChipIconView = statusBarChipIcon, + statusBarChipIconView = icons.statusBarChipIcon, uid = sbn.uid, packageName = sbn.packageName, contentIntent = sbn.notification.contentIntent, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt new file mode 100644 index 000000000000..51c448adf998 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 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.notification.domain.model + +import com.android.systemui.statusbar.notification.headsup.PinnedStatus + +/** A class representing the state of the top pinned row. */ +sealed interface TopPinnedState { + /** Nothing is pinned. */ + data object NothingPinned : TopPinnedState + + /** + * The top pinned row is a notification with the given key and status. + * + * @property status must have [PinnedStatus.isPinned] as true. + */ + data class Pinned(val key: String, val status: PinnedStatus) : TopPinnedState { + init { + check(status.isPinned) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index b56a838a80a5..31375cc4a03a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -162,9 +162,7 @@ constructor( val sbIcon = iconBuilder.createIconView(entry) sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE val sbChipIcon: StatusBarIconView? - if ( - Flags.statusBarCallChipNotificationIcon() && !StatusBarConnectedDisplays.isEnabled - ) { + if (!StatusBarConnectedDisplays.isEnabled) { sbChipIcon = iconBuilder.createIconView(entry) sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE } else { @@ -186,7 +184,7 @@ constructor( try { setIcon(entry, normalIconDescriptor, sbIcon) - if (Flags.statusBarCallChipNotificationIcon() && sbChipIcon != null) { + if (sbChipIcon != null) { setIcon(entry, normalIconDescriptor, sbChipIcon) } setIcon(entry, sensitiveIconDescriptor, shelfIcon) 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 6f29f618ee0d..afc5bc67c0a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -750,7 +750,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp BiometricUnlockSource.Companion.fromBiometricSourceType(biometricSourceType) ); } else if (biometricSourceType == BiometricSourceType.FINGERPRINT - && mUpdateMonitor.isUdfpsSupported()) { + && mUpdateMonitor.isOpticalUdfpsSupported()) { long currUptimeMillis = mSystemClock.uptimeMillis(); if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) { mNumConsecutiveFpFailures += 1; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index c57cede754d3..f56c2d5dc5e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -18,8 +18,6 @@ package com.android.systemui.statusbar.phone.ongoingcall import android.app.ActivityManager import android.app.IActivityManager -import android.app.Notification -import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.app.PendingIntent import android.app.UidObserver import android.content.Context @@ -44,9 +42,6 @@ import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.view.ChipChronometer import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.shared.CallType @@ -60,7 +55,9 @@ import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -/** A controller to handle the ongoing call chip in the collapsed status bar. +/** + * A controller to handle the ongoing call chip in the collapsed status bar. + * * @deprecated Use [OngoingCallInteractor] instead, which follows recommended architecture patterns */ @Deprecated("Use OngoingCallInteractor instead") @@ -71,7 +68,6 @@ constructor( @Application private val scope: CoroutineScope, private val context: Context, private val ongoingCallRepository: OngoingCallRepository, - private val notifCollection: CommonNotifCollection, private val activeNotificationsInteractor: ActiveNotificationsInteractor, private val systemClock: SystemClock, private val activityStarter: ActivityStarter, @@ -90,105 +86,24 @@ constructor( private val mListeners: MutableList<OngoingCallListener> = mutableListOf() private val uidObserver = CallAppUidObserver() - private val notifListener = - object : NotifCollectionListener { - // Temporary workaround for b/178406514 for testing purposes. - // - // b/178406514 means that posting an incoming call notif then updating it to an ongoing - // call notif does not work (SysUI never receives the update). This workaround allows us - // to trigger the ongoing call chip when an ongoing call notif is *added* rather than - // *updated*, allowing us to test the chip. - // - // TODO(b/183229367): Remove this function override when b/178406514 is fixed. - override fun onEntryAdded(entry: NotificationEntry) { - onEntryUpdated(entry, true) - } - - override fun onEntryUpdated(entry: NotificationEntry) { - StatusBarUseReposForCallChip.assertInLegacyMode() - // We have a new call notification or our existing call notification has been - // updated. - // TODO(b/183229367): This likely won't work if you take a call from one app then - // switch to a call from another app. - if ( - callNotificationInfo == null && isCallNotification(entry) || - (entry.sbn.key == callNotificationInfo?.key) - ) { - val newOngoingCallInfo = - CallNotificationInfo( - entry.sbn.key, - entry.sbn.notification.getWhen(), - // In this old listener pattern, we don't have access to the - // notification icon. - notificationIconView = null, - entry.sbn.notification.contentIntent, - entry.sbn.uid, - entry.sbn.notification.extras.getInt( - Notification.EXTRA_CALL_TYPE, - -1, - ) == CALL_TYPE_ONGOING, - statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false, - ) - if (newOngoingCallInfo == callNotificationInfo) { - return - } - - callNotificationInfo = newOngoingCallInfo - if (newOngoingCallInfo.isOngoing) { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = newOngoingCallInfo.key }, - { "Call notif *is* ongoing -> showing chip. key=$str1" }, - ) - updateChip() - } else { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = newOngoingCallInfo.key }, - { "Call notif not ongoing -> hiding chip. key=$str1" }, - ) - removeChip() - } - } - } - - override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { - if (entry.sbn.key == callNotificationInfo?.key) { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = entry.sbn.key }, - { "Call notif removed -> hiding chip. key=$str1" }, - ) - removeChip() - } - } - } override fun start() { - if (StatusBarChipsModernization.isEnabled) - return + if (StatusBarChipsModernization.isEnabled) return dumpManager.registerDumpable(this) - if (Flags.statusBarUseReposForCallChip()) { - scope.launch { - // Listening to [ActiveNotificationsInteractor] instead of using - // [NotifCollectionListener#onEntryUpdated] is better for two reasons: - // 1. ActiveNotificationsInteractor automatically filters the notification list to - // just notifications for the current user, which ensures we don't show a call chip - // for User 1's call while User 2 is active (see b/328584859). - // 2. ActiveNotificationsInteractor only emits notifications that are currently - // present in the shade, which means we know we've already inflated the icon that we - // might use for the call chip (see b/354930838). - activeNotificationsInteractor.ongoingCallNotification.collect { - updateInfoFromNotifModel(it) - } + scope.launch { + // Listening to [ActiveNotificationsInteractor] instead of using + // [NotifCollectionListener#onEntryUpdated] is better for two reasons: + // 1. ActiveNotificationsInteractor automatically filters the notification list to + // just notifications for the current user, which ensures we don't show a call chip + // for User 1's call while User 2 is active (see b/328584859). + // 2. ActiveNotificationsInteractor only emits notifications that are currently + // present in the shade, which means we know we've already inflated the icon that we + // might use for the call chip (see b/354930838). + activeNotificationsInteractor.ongoingCallNotification.collect { + updateInfoFromNotifModel(it) } - } else { - notifCollection.addCollectionListener(notifListener) } scope.launch { @@ -244,21 +159,12 @@ constructor( logger.log( TAG, LogLevel.DEBUG, - { - bool1 = Flags.statusBarCallChipNotificationIcon() - bool2 = currentInfo.notificationIconView != null - }, - { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" }, + { bool1 = currentInfo.notificationIconView != null }, + { "Creating OngoingCallModel.InCall. hasIcon=$bool1" }, ) - val icon = - if (Flags.statusBarCallChipNotificationIcon()) { - currentInfo.notificationIconView - } else { - null - } return OngoingCallModel.InCall( startTimeMs = currentInfo.callStartTime, - notificationIconView = icon, + notificationIconView = currentInfo.notificationIconView, intent = currentInfo.intent, notificationKey = currentInfo.key, ) @@ -597,8 +503,4 @@ constructor( } } -private fun isCallNotification(entry: NotificationEntry): Boolean { - return entry.sbn.notification.isStyle(Notification.CallStyle::class.java) -} - private const val TAG = OngoingCallRepository.TAG diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt deleted file mode 100644 index 4bdd90ebff3e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 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.phone.ongoingcall - -import com.android.systemui.Flags -import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils - -/** Helper for reading or using the status bar use repos for call chip flag state. */ -@Suppress("NOTHING_TO_INLINE") -object StatusBarUseReposForCallChip { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the refactor enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.statusBarUseReposForCallChip() - - /** - * Called to ensure code is only run when the flag is enabled. This protects users from the - * unintended behaviors caused by accidentally running new logic, while also crashing on an eng - * build to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun isUnexpectedlyInLegacyMode() = - RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) - - /** - * Called to ensure code is only run when the flag is disabled. This will throw an exception if - * the flag is enabled to ensure that the refactor author catches issues in testing. - */ - @JvmStatic - inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index b78e010572c1..71e19188f309 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -33,9 +34,11 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.compose.theme.PlatformTheme import com.android.keyguard.AlphaOptimizedLinearLayout import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips @@ -160,6 +163,45 @@ fun StatusBarRoot( darkIconDispatcher, ) iconController.addIconGroup(darkIconManager) + + if (StatusBarChipsModernization.isEnabled) { + val startSideExceptHeadsUp = + phoneStatusBarView.requireViewById<LinearLayout>( + R.id.status_bar_start_side_except_heads_up + ) + + val composeView = + ComposeView(context).apply { + layoutParams = + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + ) + + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + + setContent { + PlatformTheme { + val chips by + statusBarViewModel.ongoingActivityChips + .collectAsStateWithLifecycle() + OngoingActivityChips(chips = chips) + } + } + } + + // Add the composable container for ongoingActivityChips before the + // notification_icon_area to maintain the same ordering for ongoing activity + // chips in the status bar layout. + val notificationIconAreaIndex = + startSideExceptHeadsUp.indexOfChild( + startSideExceptHeadsUp.findViewById(R.id.notification_icon_area) + ) + startSideExceptHeadsUp.addView(composeView, notificationIconAreaIndex) + } + HomeStatusBarIconBlockListBinder.bind( statusIconContainer, darkIconManager, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index d731752ad5d5..d9d9a29ee2b6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -295,11 +295,12 @@ constructor( override val shouldShowOperatorNameView: Flow<Boolean> = combine( shouldHomeStatusBarBeVisible, - headsUpNotificationInteractor.statusBarHeadsUpState, + headsUpNotificationInteractor.statusBarHeadsUpStatus, homeStatusBarInteractor.visibilityViaDisableFlags, homeStatusBarInteractor.shouldShowOperatorName, - ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags, shouldShowOperator -> - val hideForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem + ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags, shouldShowOperator + -> + val hideForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem shouldStatusBarBeVisible && !hideForHeadsUp && visibilityViaDisableFlags.isSystemInfoAllowed && @@ -309,10 +310,10 @@ constructor( override val isClockVisible: Flow<VisibilityModel> = combine( shouldHomeStatusBarBeVisible, - headsUpNotificationInteractor.statusBarHeadsUpState, + headsUpNotificationInteractor.statusBarHeadsUpStatus, homeStatusBarInteractor.visibilityViaDisableFlags, - ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags -> - val hideClockForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem + ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags -> + val hideClockForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem val showClock = shouldStatusBarBeVisible && visibilityViaDisableFlags.isClockAllowed && diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt index 6175ea190697..a98a9e0c16d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt @@ -60,7 +60,7 @@ constructor( private val showingHeadsUpStatusBar: Flow<Boolean> = if (SceneContainerFlag.isEnabled) { - headsUpNotificationInteractor.statusBarHeadsUpState.map { it.isPinned } + headsUpNotificationInteractor.statusBarHeadsUpStatus.map { it.isPinned } } else { flowOf(false) } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt index d9a2e956cc86..a88b127ae157 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt @@ -17,10 +17,14 @@ package com.android.systemui.util.kotlin import android.content.Context +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn class Utils { companion object { @@ -32,6 +36,7 @@ class Utils { fun <A, B, C, D> toQuad(a: A, bcd: Triple<B, C, D>) = Quad(a, bcd.first, bcd.second, bcd.third) + fun <A, B, C, D> toQuad(abc: Triple<A, B, C>, d: D) = Quad(abc.first, abc.second, abc.third, d) @@ -51,7 +56,7 @@ class Utils { bcdefg.third, bcdefg.fourth, bcdefg.fifth, - bcdefg.sixth + bcdefg.sixth, ) /** @@ -81,7 +86,7 @@ class Utils { fun <A, B, C, D> Flow<A>.sample( b: Flow<B>, c: Flow<C>, - d: Flow<D> + d: Flow<D>, ): Flow<Quad<A, B, C, D>> { return this.sample(combine(b, c, d, ::Triple), ::toQuad) } @@ -134,6 +139,20 @@ class Utils { ): Flow<Septuple<A, B, C, D, E, F, G>> { return this.sample(combine(b, c, d, e, f, g, ::Sextuple), ::toSeptuple) } + + /** + * Combines 2 state flows, applying [transform] between the initial values to set the + * initial value of the resulting StateFlow. + */ + fun <A, B, R> combineState( + f1: StateFlow<A>, + f2: StateFlow<B>, + scope: CoroutineScope, + sharingStarted: SharingStarted, + transform: (A, B) -> R, + ): StateFlow<R> = + combine(f1, f2) { a, b -> transform(a, b) } + .stateIn(scope, sharingStarted, transform(f1.value, f2.value)) } } @@ -144,7 +163,7 @@ data class Quint<A, B, C, D, E>( val second: B, val third: C, val fourth: D, - val fifth: E + val fifth: E, ) data class Sextuple<A, B, C, D, E, F>( diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt index e8d19dd5e0e4..96630ca36b97 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt @@ -51,6 +51,8 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch private const val CLOSE_DRAWER_DELAY = 300L +// Ensure roundness and color of button is updated when progress is changed by a minimum fraction. +private const val BUTTON_MIN_VISIBLE_CHANGE = 0.05F @OptIn(ExperimentalCoroutinesApi::class) @VolumeDialogScope @@ -58,12 +60,12 @@ class VolumeDialogRingerViewBinder @Inject constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { private val roundnessSpringForce = - SpringForce(0F).apply { + SpringForce(1F).apply { stiffness = 800F dampingRatio = 0.6F } private val colorSpringForce = - SpringForce(0F).apply { + SpringForce(1F).apply { stiffness = 3800F dampingRatio = 1F } @@ -257,30 +259,35 @@ constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { // We only need to execute on roundness animation end and volume dialog background // progress update once because these changes should be applied once on volume dialog // background and ringer drawer views. - val selectedCornerRadius = (selectedButton.background as GradientDrawable).cornerRadius - if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) { - selectedButton.animateTo( - selectedButtonUiModel, - if (uiModel.currentButtonIndex == count - 1) { - onProgressChanged - } else { - { _, _ -> } - }, - ) - } - val unselectedCornerRadius = - (unselectedButton.background as GradientDrawable).cornerRadius - if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) { - unselectedButton.animateTo( - unselectedButtonUiModel, - if (previousIndex == count - 1) { - onProgressChanged - } else { - { _, _ -> } - }, - ) - } coroutineScope { + val selectedCornerRadius = + (selectedButton.background as GradientDrawable).cornerRadius + if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) { + launch { + selectedButton.animateTo( + selectedButtonUiModel, + if (uiModel.currentButtonIndex == count - 1) { + onProgressChanged + } else { + { _, _ -> } + }, + ) + } + } + val unselectedCornerRadius = + (unselectedButton.background as GradientDrawable).cornerRadius + if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) { + launch { + unselectedButton.animateTo( + unselectedButtonUiModel, + if (previousIndex == count - 1) { + onProgressChanged + } else { + { _, _ -> } + }, + ) + } + } launch { delay(CLOSE_DRAWER_DELAY) bindButtons(viewModel, uiModel, onAnimationEnd, isAnimated = true) @@ -383,11 +390,14 @@ constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> }, ) { val roundnessAnimation = - SpringAnimation(FloatValueHolder(0F)).setSpring(roundnessSpringForce) - val colorAnimation = SpringAnimation(FloatValueHolder(0F)).setSpring(colorSpringForce) + SpringAnimation(FloatValueHolder(0F), 1F).setSpring(roundnessSpringForce) + val colorAnimation = SpringAnimation(FloatValueHolder(0F), 1F).setSpring(colorSpringForce) val radius = (background as GradientDrawable).cornerRadius val cornerRadiusDiff = ringerButtonUiModel.cornerRadius - (background as GradientDrawable).cornerRadius + + roundnessAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE + colorAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE coroutineScope { launch { colorAnimation.suspendAnimate { value -> diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index 46d7d5f680ce..428dc6ecb5b6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -80,7 +80,6 @@ constructor( MutableStateFlow(WindowInsets.Builder().build()) // Root view of the Volume Dialog. val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root) - root.alpha = 0f animateVisibility(root, dialog, viewModel.dialogVisibilityModel) diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 047b78eeb04e..bac2c47f51c7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -24,7 +24,7 @@ import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND_INACTIVE +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.Flags @@ -99,8 +99,9 @@ import org.mockito.kotlin.clearInvocations class ClockEventControllerTest : SysuiTestCase() { private val kosmos = testKosmos() - private val zenModeRepository = kosmos.fakeZenModeRepository private val testScope = kosmos.testScope + private val zenModeRepository by lazy { kosmos.fakeZenModeRepository } + private val zenModeInteractor by lazy { kosmos.zenModeInteractor } @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -108,7 +109,6 @@ class ClockEventControllerTest : SysuiTestCase() { private lateinit var repository: FakeKeyguardRepository private val clockBuffers = ClockMessageBuffers(LogcatOnlyMessageBuffer(LogLevel.DEBUG)) private lateinit var underTest: ClockEventController - private lateinit var dndModeId: String @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var batteryController: BatteryController @@ -158,9 +158,6 @@ class ClockEventControllerTest : SysuiTestCase() { whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null)) whenever(userTracker.userId).thenReturn(1) - dndModeId = MANUAL_DND_INACTIVE.id - zenModeRepository.addMode(MANUAL_DND_INACTIVE) - repository = kosmos.fakeKeyguardRepository kosmos.fakeFeatureFlagsClassic.set(Flags.REGION_SAMPLING, false) @@ -179,7 +176,7 @@ class ClockEventControllerTest : SysuiTestCase() { clockBuffers, kosmos.fakeFeatureFlagsClassic, zenModeController, - kosmos.zenModeInteractor, + zenModeInteractor, userTracker, ) underTest.clock = clock @@ -504,7 +501,7 @@ class ClockEventControllerTest : SysuiTestCase() { runCurrent() clearInvocations(events) - zenModeRepository.activateMode(dndModeId) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() verify(events) @@ -512,7 +509,7 @@ class ClockEventControllerTest : SysuiTestCase() { eq(ZenData(ZenMode.IMPORTANT_INTERRUPTIONS, R.string::dnd_is_on.name)) ) - zenModeRepository.deactivateMode(dndModeId) + zenModeRepository.deactivateMode(MANUAL_DND) runCurrent() verify(events).onZenDataChanged(eq(ZenData(ZenMode.OFF, R.string::dnd_is_off.name))) diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java index 9d9fb9c23a73..6ad2128759a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java @@ -30,9 +30,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.pm.ActivityInfo; @@ -51,11 +48,8 @@ import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Interpolator; import android.window.InputTransferToken; -import androidx.annotation.NonNull; import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; @@ -80,19 +74,17 @@ import java.util.function.Supplier; @RunWith(AndroidTestingRunner.class) @FlakyTest(bugId = 385115361) public class FullscreenMagnificationControllerTest extends SysuiTestCase { - private static final long ANIMATION_DURATION_MS = 100L; private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER; - private static final long ANIMATION_TIMEOUT_MS = - 5L * ANIMATION_DURATION_MS * HW_TIMEOUT_MULTIPLIER; private static final String UNIQUE_DISPLAY_ID_PRIMARY = "000"; private static final String UNIQUE_DISPLAY_ID_SECONDARY = "111"; private static final int CORNER_RADIUS_PRIMARY = 10; private static final int CORNER_RADIUS_SECONDARY = 20; + private static final int DISABLED = 0; + private static final int ENABLED = 3; private FullscreenMagnificationController mFullscreenMagnificationController; private SurfaceControlViewHost mSurfaceControlViewHost; - private ValueAnimator mShowHideBorderAnimator; private SurfaceControl.Transaction mTransaction; private TestableWindowManager mWindowManager; @Mock @@ -136,7 +128,6 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); mTransaction = new SurfaceControl.Transaction(); - mShowHideBorderAnimator = spy(newNullTargetObjectAnimator()); mFullscreenMagnificationController = new FullscreenMagnificationController( mContext, mContext.getMainThreadHandler(), @@ -146,141 +137,68 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { mContext.getSystemService(WindowManager.class), mIWindowManager, scvhSupplier, - mTransaction, - mShowHideBorderAnimator); + mTransaction); } @After public void tearDown() { - getInstrumentation().runOnMainSync( - () -> mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(false)); + getInstrumentation().runOnMainSync(() -> + mFullscreenMagnificationController.cleanUpBorder()); + } + + @Test + public void createShowTargetAnimator_runAnimator_alphaIsEqualToOne() { + View view = new View(mContext); + view.setAlpha(0f); + ValueAnimator animator = mFullscreenMagnificationController.createShowTargetAnimator(view); + animator.end(); + assertThat(view.getAlpha()).isEqualTo(1f); + } + + @Test + public void createHideTargetAnimator_runAnimator_alphaIsEqualToZero() { + View view = new View(mContext); + view.setAlpha(1f); + ValueAnimator animator = mFullscreenMagnificationController.createHideTargetAnimator(view); + animator.end(); + assertThat(view.getAlpha()).isEqualTo(0f); } @Test - public void enableFullscreenMagnification_visibleBorder() + public void enableFullscreenMagnification_stateEnabled() throws InterruptedException, RemoteException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).start(); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); + + assertThat(mFullscreenMagnificationController.getState()).isEqualTo(ENABLED); verify(mIWindowManager) .watchRotation(any(IRotationWatcher.class), eq(Display.DEFAULT_DISPLAY)); - assertThat(mSurfaceControlViewHost.getView().isVisibleToUser()).isTrue(); } @Test - public void disableFullscreenMagnification_reverseAnimationAndReleaseScvh() + public void disableFullscreenMagnification_stateDisabled() throws InterruptedException, RemoteException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch enableAnimationEndLatch = new CountDownLatch(1); - CountDownLatch disableAnimationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { - if (isReverse) { - disableAnimationEndLatch.countDown(); - } else { - enableAnimationEndLatch.countDown(); - } - } - }); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for enabling animation to be finished") - .that(enableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).start(); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); - getInstrumentation().runOnMainSync(() -> - // Disable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(false)); + getInstrumentation().runOnMainSync(() -> { + // Disable fullscreen magnification + mFullscreenMagnificationController + .onFullscreenMagnificationActivationChanged(false); + }); + waitForIdleSync(); + assertThat(mFullscreenMagnificationController.mShowHideBorderAnimator).isNotNull(); + mFullscreenMagnificationController.mShowHideBorderAnimator.end(); + waitForIdleSync(); - assertWithMessage("Failed to wait for disabling animation to be finished") - .that(disableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).reverse(); + assertThat(mFullscreenMagnificationController.getState()).isEqualTo(DISABLED); verify(mSurfaceControlViewHost).release(); verify(mIWindowManager).removeRotationWatcher(any(IRotationWatcher.class)); } @Test - public void onFullscreenMagnificationActivationChangeTrue_deactivating_reverseAnimator() - throws InterruptedException { - // Simulate the hiding border animation is running - when(mShowHideBorderAnimator.isRunning()).thenReturn(true); - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - - getInstrumentation().runOnMainSync( - () -> mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).reverse(); - } - - @Test public void onScreenSizeChanged_activated_borderChangedToExpectedSize() throws InterruptedException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); + final Rect testWindowBounds = new Rect( mWindowManager.getCurrentWindowMetrics().getBounds()); testWindowBounds.set(testWindowBounds.left, testWindowBounds.top, @@ -304,29 +222,8 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { @Test public void enableFullscreenMagnification_applyPrimaryCornerRadius() throws InterruptedException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - - // Verify the initial corner radius is applied GradientDrawable backgroundDrawable = (GradientDrawable) mSurfaceControlViewHost.getView().getBackground(); assertThat(backgroundDrawable.getCornerRadius()).isEqualTo(CORNER_RADIUS_PRIMARY); @@ -334,28 +231,8 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { @EnableFlags(Flags.FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED) @Test - public void onDisplayChanged_updateCornerRadiusToSecondary() throws InterruptedException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); + public void onDisplayChanged_applyCornerRadiusToBorder() throws InterruptedException { + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); ArgumentCaptor<DisplayManager.DisplayListener> displayListenerCaptor = ArgumentCaptor.forClass(DisplayManager.DisplayListener.class); @@ -372,22 +249,34 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { .addOverride( com.android.internal.R.dimen.rounded_corner_radius, CORNER_RADIUS_SECONDARY); + getInstrumentation().runOnMainSync(() -> displayListenerCaptor.getValue().onDisplayChanged(Display.DEFAULT_DISPLAY)); waitForIdleSync(); + // Verify the corner radius is updated GradientDrawable backgroundDrawable2 = (GradientDrawable) mSurfaceControlViewHost.getView().getBackground(); assertThat(backgroundDrawable2.getCornerRadius()).isEqualTo(CORNER_RADIUS_SECONDARY); } + private void enableFullscreenMagnificationAndWaitForTransactionAndAnimation() + throws InterruptedException { + CountDownLatch transactionCommittedLatch = new CountDownLatch(1); + mTransaction.addTransactionCommittedListener( + Runnable::run, transactionCommittedLatch::countDown); + + getInstrumentation().runOnMainSync(() -> + //Enable fullscreen magnification + mFullscreenMagnificationController + .onFullscreenMagnificationActivationChanged(true)); - private ValueAnimator newNullTargetObjectAnimator() { - final ValueAnimator animator = - ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f); - Interpolator interpolator = new DecelerateInterpolator(2.5f); - animator.setInterpolator(interpolator); - animator.setDuration(ANIMATION_DURATION_MS); - return animator; + assertWithMessage("Failed to wait for transaction committed") + .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) + .isTrue(); + waitForIdleSync(); + assertThat(mFullscreenMagnificationController.mShowHideBorderAnimator).isNotNull(); + mFullscreenMagnificationController.mShowHideBorderAnimator.end(); + waitForIdleSync(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java index 856c37934251..9f6ad56335d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -82,7 +82,7 @@ public class MenuAnimationControllerTest extends SysuiTestCase { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); - final SecureSettings secureSettings = TestUtils.mockSecureSettings(); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, secureSettings, mHearingAidDeviceManager); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 33cfb3890e71..1500340c9d89 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -144,7 +144,7 @@ public class MenuViewLayerTest extends SysuiTestCase { private HearingAidDeviceManager mHearingAidDeviceManager; @Mock private PackageManager mMockPackageManager; - private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(); + private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(mContext); private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt index 57a12df0cfee..c4ef4f978ff8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt @@ -35,7 +35,6 @@ import android.platform.test.annotations.EnableFlags import androidx.test.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any import com.android.systemui.statusbar.core.StatusBarConnectedDisplays @@ -112,20 +111,8 @@ class IconManagerTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun testCreateIcons_chipNotifIconFlagDisabled_statusBarChipIconIsNull() { - val entry = - notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) - entry?.let { iconManager.createIcons(it) } - testScope.runCurrent() - - assertThat(entry?.icons?.statusBarChipIcon).isNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagDisabled_statusBarChipIconIsNotNull() { + fun testCreateIcons_cdFlagDisabled_statusBarChipIconIsNotNull() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) entry?.let { iconManager.createIcons(it) } @@ -135,8 +122,8 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagEnabled_statusBarChipIconIsNull() { + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testCreateIcons_cdFlagEnabled_statusBarChipIconIsNull() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) entry?.let { iconManager.createIcons(it) } @@ -217,7 +204,6 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testCreateIcons_cdFlagDisabled_sensitiveImportantConversation() { val entry = @@ -233,7 +219,7 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testCreateIcons_cdFlagEnabled_sensitiveImportantConversation() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) @@ -248,7 +234,6 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testUpdateIcons_cdFlagDisabled_sensitiveImportantConversation() { val entry = @@ -266,7 +251,7 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testUpdateIcons_cdFlagEnabled_sensitiveImportantConversation() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt index 5ac41ec6741c..f380789968f5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt @@ -23,11 +23,11 @@ import com.android.systemui.util.mockito.mock val Kosmos.mockActivityTransitionAnimatorController by Kosmos.Fixture { mock<ActivityTransitionAnimator.Controller>() } -val Kosmos.activityTransitionAnimator by +var Kosmos.activityTransitionAnimator by Kosmos.Fixture { ActivityTransitionAnimator( // The main thread is checked in a bunch of places inside the different transitions // animators, so we have to pass the real main executor here. - mainExecutor = testCase.context.mainExecutor, + mainExecutor = testCase.context.mainExecutor ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index 2a7e3e903737..490b89bf6b13 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -23,6 +23,7 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi import com.android.systemui.dump.dumpManager import com.android.systemui.keyevent.domain.interactor.keyEventInteractor import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.util.time.systemClock @@ -34,6 +35,8 @@ val Kosmos.deviceEntryHapticsInteractor by DeviceEntryHapticsInteractor( biometricSettingsRepository = biometricSettingsRepository, deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor, + deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, + keyguardBypassInteractor = keyguardBypassInteractor, deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor = deviceEntrySourceInteractor, fingerprintPropertyRepository = fingerprintPropertyRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt index 8ae1332c387a..639bb691f455 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt @@ -18,13 +18,18 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel val Kosmos.editModeButtonViewModelFactory by Kosmos.Fixture { object : EditModeButtonViewModel.Factory { override fun create(): EditModeButtonViewModel { - return EditModeButtonViewModel(editModeViewModel, falsingInteractor) + return EditModeButtonViewModel( + editModeViewModel, + falsingInteractor, + activityStarter, + ) } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 82b5f6332b23..72c75000ebf4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.scene.domain.startable import com.android.internal.logging.uiEventLogger +import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor @@ -85,5 +86,6 @@ val Kosmos.sceneContainerStartable by Fixture { vibratorHelper = vibratorHelper, msdlPlayer = msdlPlayer, disabledContentInteractor = disabledContentInteractor, + activityTransitionAnimator = activityTransitionAnimator, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt index ab193d294b8c..b3d89dbb834d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt @@ -203,6 +203,7 @@ class ShadeTestUtilSceneImpl( val isUserInputOngoing = MutableStateFlow(true) override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) { + shadeRepository.setLegacyIsQsExpanded(qsExpansion > 0f) if (shadeExpansion == 1f) { setIdleScene(Scenes.Shade) } else if (qsExpansion == 1f) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt index 4af5e7d9d725..6e44df833582 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorKosmos.kt @@ -23,11 +23,21 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker import com.android.systemui.shade.ShadeWindowLayoutParams import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository +import java.util.Optional +import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever val Kosmos.shadeLayoutParams by Kosmos.Fixture { ShadeWindowLayoutParams.create(mockedContext) } -val Kosmos.mockedWindowContext by Kosmos.Fixture { mock<WindowContext>() } +val Kosmos.mockedWindowContext by + Kosmos.Fixture { + mock<WindowContext>().apply { + whenever(reparentToDisplay(any())).thenAnswer { displayIdParam -> + whenever(displayId).thenReturn(displayIdParam.arguments[0] as Int) + } + } + } val Kosmos.mockedShadeDisplayChangeLatencyTracker by Kosmos.Fixture { mock<ShadeDisplayChangeLatencyTracker>() } val Kosmos.shadeDisplaysInteractor by @@ -38,5 +48,6 @@ val Kosmos.shadeDisplaysInteractor by testScope.backgroundScope, testScope.backgroundScope.coroutineContext, mockedShadeDisplayChangeLatencyTracker, + Optional.of(shadeExpandedStateInteractor), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index af6d6249b4a8..1dc7229a6506 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.ShadeModule @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.policy.data.repository.userSetupRepository import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor import com.android.systemui.user.domain.interactor.userSwitcherInteractor +import org.mockito.kotlin.mock var Kosmos.baseShadeInteractor: BaseShadeInteractor by Kosmos.Fixture { @@ -71,3 +73,7 @@ val Kosmos.shadeInteractorImpl by shadeModeInteractor = shadeModeInteractor, ) } +var Kosmos.mockShadeInteractor: ShadeInteractor by Kosmos.Fixture { mock() } +val Kosmos.shadeExpandedStateInteractor by + Kosmos.Fixture { ShadeExpandedStateInteractorImpl(shadeInteractor, testScope.backgroundScope) } +val Kosmos.fakeShadeExpandedStateInteractor by Kosmos.Fixture { FakeShadeExpandedStateInteractor() } diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 59043a8356ae..8e998426685b 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -345,14 +345,41 @@ java_library { ], } +// We define our own version of platform_compat_config's here, because: +// - The original version (e.g. "framework-platform-compat-config) is built from +// the output file of the device side jar, rather than the host jar, meaning +// they're slow to build because they depend on D8/R8 output. +// - The original services one ("services-platform-compat-config") is built from services.jar, +// which includes service.permission, which is very slow to rebuild because of kotlin. +// +// Because we're re-defining the same compat-IDs that are defined elsewhere, +// they should all have `include_in_merged_xml: false`. Otherwise, generating +// merged_compat_config.xml would fail due to duplicate IDs. +// +// These module names must end with "compat-config" because these will be used as the filename, +// and at runtime, we only loads files that match `*compat-config.xml`. +platform_compat_config { + name: "ravenwood-framework-platform-compat-config", + src: ":framework-minus-apex-for-host", + include_in_merged_xml: false, + visibility: ["//visibility:private"], +} + +platform_compat_config { + name: "ravenwood-services.core-platform-compat-config", + src: ":services.core-for-host", + include_in_merged_xml: false, + visibility: ["//visibility:private"], +} + filegroup { name: "ravenwood-data", device_common_srcs: [ ":system-build.prop", ":framework-res", ":ravenwood-empty-res", - ":framework-platform-compat-config", - ":services-platform-compat-config", + ":ravenwood-framework-platform-compat-config", + ":ravenwood-services.core-platform-compat-config", "texts/ravenwood-build.prop", ], device_first_srcs: [ @@ -616,6 +643,10 @@ android_ravenwood_libgroup { "android.test.mock.ravenwood", "ravenwood-helper-runtime", "hoststubgen-helper-runtime.ravenwood", + + // Note, when we include other services.* jars, we'll need to add + // platform_compat_config for that module too. + // See ravenwood-services.core-platform-compat-config above. "services.core.ravenwood-jarjar", "services.fakes.ravenwood-jarjar", diff --git a/ravenwood/CleanSpec.mk b/ravenwood/CleanSpec.mk new file mode 100644 index 000000000000..50d2fab40326 --- /dev/null +++ b/ravenwood/CleanSpec.mk @@ -0,0 +1,45 @@ +# Copyright (C) 2024 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. +# + +# If you don't need to do a full clean build but would like to touch +# a file or delete some intermediate files, add a clean step to the end +# of the list. These steps will only be run once, if they haven't been +# run before. +# +# E.g.: +# $(call add-clean-step, touch -c external/sqlite/sqlite3.h) +# $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates) +# +# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with +# files that are missing or have been moved. +# +# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory. +# Use $(OUT_DIR) to refer to the "out" directory. +# +# If you need to re-do something that's already mentioned, just copy +# the command and add it to the bottom of the list. E.g., if a change +# that you made last week required touching a file and a change you +# made today requires touching the same file, just copy the old +# touch step and add it to the end of the list. +# +# ***************************************************************** +# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THE BANNER +# ***************************************************************** + +$(call add-clean-step, rm -rf $(OUT_DIR)/host/linux-x86/testcases/ravenwood-runtime) + +# ****************************************************************** +# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THIS BANNER +# ****************************************************************** diff --git a/ravenwood/tools/hoststubgen/scripts/dump-jar b/ravenwood/tools/hoststubgen/scripts/dump-jar index 87652451359d..998357b70dff 100755 --- a/ravenwood/tools/hoststubgen/scripts/dump-jar +++ b/ravenwood/tools/hoststubgen/scripts/dump-jar @@ -89,7 +89,7 @@ filter_output() { # - Some other transient lines # - Sometimes the javap shows mysterious warnings, so remove them too. # - # `/PATTERN-1/,/PATTERN-1/{//!d}` is a trick to delete lines between two patterns, without + # `/PATTERN-1/,/PATTERN-2/{//!d}` is a trick to delete lines between two patterns, without # the start and the end lines. sed -e 's/#[0-9][0-9]*/#x/g' \ -e 's/^\( *\)[0-9][0-9]*:/\1x:/' \ diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 6d8d7b768b91..cc704b2b32ed 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -27,7 +27,7 @@ import com.android.hoststubgen.filters.ImplicitOutputFilter import com.android.hoststubgen.filters.KeepNativeFilter import com.android.hoststubgen.filters.OutputFilter import com.android.hoststubgen.filters.SanitizationFilter -import com.android.hoststubgen.filters.TextFileFilterPolicyParser +import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder import com.android.hoststubgen.filters.printAsTextPolicy import com.android.hoststubgen.utils.ClassFilter import com.android.hoststubgen.visitors.BaseAdapter @@ -179,9 +179,9 @@ class HostStubGen(val options: HostStubGenOptions) { // Next, "text based" filter, which allows to override polices without touching // the target code. if (options.policyOverrideFiles.isNotEmpty()) { - val parser = TextFileFilterPolicyParser(allClasses, filter) - options.policyOverrideFiles.forEach(parser::parse) - filter = parser.createOutputFilter() + val builder = TextFileFilterPolicyBuilder(allClasses, filter) + options.policyOverrideFiles.forEach(builder::parse) + filter = builder.createOutputFilter() } // Apply the implicit filter. diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt index 7462a8ce12c5..be1b6ca93869 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt @@ -23,10 +23,12 @@ import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.log import com.android.hoststubgen.normalizeTextLine import com.android.hoststubgen.whitespaceRegex -import java.io.File +import org.objectweb.asm.tree.ClassNode +import java.io.BufferedReader +import java.io.FileReader import java.io.PrintWriter +import java.io.Reader import java.util.regex.Pattern -import org.objectweb.asm.tree.ClassNode /** * Print a class node as a "keep" policy. @@ -48,7 +50,7 @@ fun printAsTextPolicy(pw: PrintWriter, cn: ClassNode) { private const val FILTER_REASON = "file-override" -private enum class SpecialClass { +enum class SpecialClass { NotSpecial, Aidl, FeatureFlags, @@ -56,10 +58,58 @@ private enum class SpecialClass { RFile, } -class TextFileFilterPolicyParser( +/** + * This receives [TextFileFilterPolicyBuilder] parsing result. + */ +interface PolicyFileProcessor { + /** "package" directive. */ + fun onPackage(name: String, policy: FilterPolicyWithReason) + + /** "rename" directive. */ + fun onRename(pattern: Pattern, prefix: String) + + /** "class" directive. */ + fun onSimpleClassStart(className: String) + fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) + fun onSimpleClassEnd(className: String) + + fun onSubClassPolicy(superClassName: String, policy: FilterPolicyWithReason) + fun onRedirectionClass(fromClassName: String, toClassName: String) + fun onClassLoadHook(className: String, callback: String) + fun onSpecialClassPolicy(type: SpecialClass, policy: FilterPolicyWithReason) + + /** "field" directive. */ + fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) + + /** "method" directive. */ + fun onSimpleMethodPolicy( + className: String, + methodName: String, + methodDesc: String, + policy: FilterPolicyWithReason, + ) + fun onMethodInClassReplace( + className: String, + methodName: String, + methodDesc: String, + targetName: String, + policy: FilterPolicyWithReason, + ) + fun onMethodOutClassReplace( + className: String, + methodName: String, + methodDesc: String, + replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + policy: FilterPolicyWithReason, + ) +} + +class TextFileFilterPolicyBuilder( private val classes: ClassNodes, fallback: OutputFilter ) { + private val parser = TextFileFilterPolicyParser() + private val subclassFilter = SubclassFilter(classes, fallback) private val packageFilter = PackageFilter(subclassFilter) private val imf = InMemoryOutputFilter(classes, packageFilter) @@ -71,30 +121,19 @@ class TextFileFilterPolicyParser( private val methodReplaceSpec = mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>() - private lateinit var currentClassName: String - /** - * Read a given "policy" file and return as an [OutputFilter] + * Parse a given policy file. This method can be called multiple times to read from + * multiple files. To get the resulting filter, use [createOutputFilter] */ fun parse(file: String) { - log.i("Loading offloaded annotations from $file ...") - log.withIndent { - var lineNo = 0 - try { - File(file).forEachLine { - lineNo++ - val line = normalizeTextLine(it) - if (line.isEmpty()) { - return@forEachLine // skip empty lines. - } - parseLine(line) - } - } catch (e: ParseException) { - throw e.withSourceInfo(file, lineNo) - } - } + // We may parse multiple files, but we reuse the same parser, because the parser + // will make sure there'll be no dupplicating "special class" policies. + parser.parse(FileReader(file), file, Processor()) } + /** + * Generate the resulting [OutputFilter]. + */ fun createOutputFilter(): OutputFilter { var ret: OutputFilter = imf if (typeRenameSpec.isNotEmpty()) { @@ -112,14 +151,200 @@ class TextFileFilterPolicyParser( return ret } + private inner class Processor : PolicyFileProcessor { + override fun onPackage(name: String, policy: FilterPolicyWithReason) { + packageFilter.addPolicy(name, policy) + } + + override fun onRename(pattern: Pattern, prefix: String) { + typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec( + pattern, prefix + ) + } + + override fun onSimpleClassStart(className: String) { + } + + override fun onSimpleClassEnd(className: String) { + } + + override fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) { + imf.setPolicyForClass(className, policy) + } + + override fun onSubClassPolicy( + superClassName: String, + policy: FilterPolicyWithReason, + ) { + log.i("class extends $superClassName") + subclassFilter.addPolicy( superClassName, policy) + } + + override fun onRedirectionClass(fromClassName: String, toClassName: String) { + imf.setRedirectionClass(fromClassName, toClassName) + } + + override fun onClassLoadHook(className: String, callback: String) { + imf.setClassLoadHook(className, callback) + } + + override fun onSpecialClassPolicy( + type: SpecialClass, + policy: FilterPolicyWithReason, + ) { + log.i("class special $type $policy") + when (type) { + SpecialClass.NotSpecial -> {} // Shouldn't happen + + SpecialClass.Aidl -> { + aidlPolicy = policy + } + + SpecialClass.FeatureFlags -> { + featureFlagsPolicy = policy + } + + SpecialClass.Sysprops -> { + syspropsPolicy = policy + } + + SpecialClass.RFile -> { + rFilePolicy = policy + } + } + } + + override fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) { + imf.setPolicyForField(className, fieldName, policy) + } + + override fun onSimpleMethodPolicy( + className: String, + methodName: String, + methodDesc: String, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + } + + override fun onMethodInClassReplace( + className: String, + methodName: String, + methodDesc: String, + targetName: String, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + + // Make sure to keep the target method. + imf.setPolicyForMethod( + className, + targetName, + methodDesc, + FilterPolicy.Keep.withReason(FILTER_REASON) + ) + // Set up the rename. + imf.setRenameTo(className, targetName, methodDesc, methodName) + } + + override fun onMethodOutClassReplace( + className: String, + methodName: String, + methodDesc: String, + replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + methodReplaceSpec.add(replaceSpec) + } + } +} + +/** + * Parses a filer policy text file. + */ +class TextFileFilterPolicyParser { + private lateinit var processor: PolicyFileProcessor + private var currentClassName: String? = null + + private var aidlPolicy: FilterPolicyWithReason? = null + private var featureFlagsPolicy: FilterPolicyWithReason? = null + private var syspropsPolicy: FilterPolicyWithReason? = null + private var rFilePolicy: FilterPolicyWithReason? = null + + /** Name of the file that's currently being processed. */ + var filename: String? = null + private set + + /** 1-based line number in the current file */ + var lineNumber = -1 + private set + + /** + * Parse a given "policy" file. + */ + fun parse(reader: Reader, inputName: String, processor: PolicyFileProcessor) { + filename = inputName + + log.i("Parsing text policy file $inputName ...") + this.processor = processor + BufferedReader(reader).use { rd -> + lineNumber = 0 + try { + while (true) { + var line = rd.readLine() + if (line == null) { + break + } + lineNumber++ + line = normalizeTextLine(line) // Remove comment and trim. + if (line.isEmpty()) { + continue + } + parseLine(line) + } + finishLastClass() + } catch (e: ParseException) { + throw e.withSourceInfo(inputName, lineNumber) + } + } + } + + private fun finishLastClass() { + currentClassName?.let { className -> + processor.onSimpleClassEnd(className) + currentClassName = null + } + } + + private fun ensureInClass(directive: String): String { + return currentClassName ?: + throw ParseException("Directive '$directive' must follow a 'class' directive") + } + private fun parseLine(line: String) { val fields = line.split(whitespaceRegex).toTypedArray() when (fields[0].lowercase()) { - "p", "package" -> parsePackage(fields) - "c", "class" -> parseClass(fields) - "f", "field" -> parseField(fields) - "m", "method" -> parseMethod(fields) - "r", "rename" -> parseRename(fields) + "p", "package" -> { + finishLastClass() + parsePackage(fields) + } + "c", "class" -> { + finishLastClass() + parseClass(fields) + } + "f", "field" -> { + ensureInClass("field") + parseField(fields) + } + "m", "method" -> { + ensureInClass("method") + parseMethod(fields) + } + "r", "rename" -> { + finishLastClass() + parseRename(fields) + } else -> throw ParseException("Unknown directive \"${fields[0]}\"") } } @@ -184,20 +409,20 @@ class TextFileFilterPolicyParser( if (!policy.isUsableWithClasses) { throw ParseException("Package can't have policy '$policy'") } - packageFilter.addPolicy(name, policy.withReason(FILTER_REASON)) + processor.onPackage(name, policy.withReason(FILTER_REASON)) } private fun parseClass(fields: Array<String>) { if (fields.size < 3) { throw ParseException("Class ('c') expects 2 fields.") } - currentClassName = fields[1] + val className = fields[1] // superClass is set when the class name starts with a "*". - val superClass = resolveExtendingClass(currentClassName) + val superClass = resolveExtendingClass(className) // :aidl, etc? - val classType = resolveSpecialClass(currentClassName) + val classType = resolveSpecialClass(className) if (fields[2].startsWith("!")) { if (classType != SpecialClass.NotSpecial) { @@ -208,7 +433,8 @@ class TextFileFilterPolicyParser( } // It's a redirection class. val toClass = fields[2].substring(1) - imf.setRedirectionClass(currentClassName, toClass) + + processor.onRedirectionClass(className, toClass) } else if (fields[2].startsWith("~")) { if (classType != SpecialClass.NotSpecial) { // We could support it, but not needed at least for now. @@ -218,7 +444,8 @@ class TextFileFilterPolicyParser( } // It's a class-load hook val callback = fields[2].substring(1) - imf.setClassLoadHook(currentClassName, callback) + + processor.onClassLoadHook(className, callback) } else { val policy = parsePolicy(fields[2]) if (!policy.isUsableWithClasses) { @@ -229,26 +456,27 @@ class TextFileFilterPolicyParser( SpecialClass.NotSpecial -> { // TODO: Duplicate check, etc if (superClass == null) { - imf.setPolicyForClass( - currentClassName, policy.withReason(FILTER_REASON) - ) + currentClassName = className + processor.onSimpleClassStart(className) + processor.onSimpleClassPolicy(className, policy.withReason(FILTER_REASON)) } else { - subclassFilter.addPolicy( + processor.onSubClassPolicy( superClass, - policy.withReason("extends $superClass") + policy.withReason("extends $superClass"), ) } } - SpecialClass.Aidl -> { if (aidlPolicy != null) { throw ParseException( "Policy for AIDL classes already defined" ) } - aidlPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class AIDL)" ) + processor.onSpecialClassPolicy(classType, p) + aidlPolicy = p } SpecialClass.FeatureFlags -> { @@ -257,9 +485,11 @@ class TextFileFilterPolicyParser( "Policy for feature flags already defined" ) } - featureFlagsPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class feature flags)" ) + processor.onSpecialClassPolicy(classType, p) + featureFlagsPolicy = p } SpecialClass.Sysprops -> { @@ -268,9 +498,11 @@ class TextFileFilterPolicyParser( "Policy for sysprops already defined" ) } - syspropsPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class sysprops)" ) + processor.onSpecialClassPolicy(classType, p) + syspropsPolicy = p } SpecialClass.RFile -> { @@ -279,9 +511,11 @@ class TextFileFilterPolicyParser( "Policy for R file already defined" ) } - rFilePolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class R file)" ) + processor.onSpecialClassPolicy(classType, p) + rFilePolicy = p } } } @@ -296,17 +530,16 @@ class TextFileFilterPolicyParser( if (!policy.isUsableWithFields) { throw ParseException("Field can't have policy '$policy'") } - require(this::currentClassName.isInitialized) // TODO: Duplicate check, etc - imf.setPolicyForField(currentClassName, name, policy.withReason(FILTER_REASON)) + processor.onField(currentClassName!!, name, policy.withReason(FILTER_REASON)) } private fun parseMethod(fields: Array<String>) { if (fields.size < 3 || fields.size > 4) { throw ParseException("Method ('m') expects 3 or 4 fields.") } - val name = fields[1] + val methodName = fields[1] val signature: String val policyStr: String if (fields.size <= 3) { @@ -323,44 +556,48 @@ class TextFileFilterPolicyParser( throw ParseException("Method can't have policy '$policy'") } - require(this::currentClassName.isInitialized) + val className = currentClassName!! - imf.setPolicyForMethod( - currentClassName, name, signature, - policy.withReason(FILTER_REASON) - ) - if (policy == FilterPolicy.Substitute) { - val fromName = policyStr.substring(1) + val policyWithReason = policy.withReason(FILTER_REASON) + if (policy != FilterPolicy.Substitute) { + processor.onSimpleMethodPolicy(className, methodName, signature, policyWithReason) + } else { + val targetName = policyStr.substring(1) - if (fromName == name) { + if (targetName == methodName) { throw ParseException( "Substitution must have a different name" ) } - // Set the policy for the "from" method. - imf.setPolicyForMethod( - currentClassName, fromName, signature, - FilterPolicy.Keep.withReason(FILTER_REASON) - ) - - val classAndMethod = splitWithLastPeriod(fromName) + val classAndMethod = splitWithLastPeriod(targetName) if (classAndMethod != null) { // If the substitution target contains a ".", then // it's a method call redirect. - methodReplaceSpec.add( - TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec( - currentClassName.toJvmClassName(), - name, + val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec( + currentClassName!!.toJvmClassName(), + methodName, signature, classAndMethod.first.toJvmClassName(), classAndMethod.second, ) + processor.onMethodOutClassReplace( + className, + methodName, + signature, + spec, + policyWithReason, ) } else { // It's an in-class replace. // ("@RavenwoodReplace" equivalent) - imf.setRenameTo(currentClassName, fromName, signature, name) + processor.onMethodInClassReplace( + className, + methodName, + signature, + targetName, + policyWithReason, + ) } } } @@ -378,7 +615,7 @@ class TextFileFilterPolicyParser( // applied. (Which is needed for services.jar) val prefix = fields[2].trimStart('/') - typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec( + processor.onRename( pattern, prefix ) } diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh index b389a67a8e4c..8408a18142eb 100755 --- a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh +++ b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh @@ -34,10 +34,11 @@ source "${0%/*}"/../common.sh SCRIPT_NAME="${0##*/}" -GOLDEN_DIR=golden-output +GOLDEN_DIR=${GOLDEN_DIR:-golden-output} mkdir -p $GOLDEN_DIR -DIFF_CMD=${DIFF:-diff -u --ignore-blank-lines --ignore-space-change} +# TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines. +DIFF_CMD=${DIFF_CMD:-diff -u --ignore-blank-lines --ignore-space-change --ignore-matching-lines='^\(Constant.pool:\|{\)$'} update=0 three_way=0 @@ -62,7 +63,7 @@ done shift $(($OPTIND - 1)) # Build the dump files, which are the input of this test. -run m dump-jar tiny-framework-dump-test +run ${BUILD_CMD:=m} dump-jar tiny-framework-dump-test # Get the path to the generate text files. (not the golden files.) diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py index 88fa492addb8..c35d6d106c8d 100755 --- a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py +++ b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py @@ -28,8 +28,11 @@ GOLDEN_DIRS = [ # Run diff. def run_diff(file1, file2): + # TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines. command = ['diff', '-u', '--ignore-blank-lines', - '--ignore-space-change', file1, file2] + '--ignore-space-change', + '--ignore-matching-lines=^\(Constant.pool:\|{\)$', + file1, file2] print(' '.join(command)) result = subprocess.run(command, stderr=sys.stdout) diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java index 3f6ede95eaf9..8804faf2d312 100644 --- a/services/backup/java/com/android/server/backup/BackupManagerService.java +++ b/services/backup/java/com/android/server/backup/BackupManagerService.java @@ -22,9 +22,9 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; import android.app.backup.BackupManager; +import android.app.backup.BackupManagerInternal; import android.app.backup.BackupRestoreEventLogger.DataTypeResult; import android.app.backup.IBackupManager; import android.app.backup.IBackupManagerMonitor; @@ -60,6 +60,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; +import com.android.server.LocalServices; import com.android.server.SystemConfig; import com.android.server.SystemService; import com.android.server.backup.utils.RandomAccessFileUtils; @@ -91,7 +92,7 @@ import java.util.Set; * privileged callers (currently {@link DevicePolicyManager}). If called on {@link * UserHandle#USER_SYSTEM}, backup is disabled for all users. */ -public class BackupManagerService extends IBackupManager.Stub { +public class BackupManagerService extends IBackupManager.Stub implements BackupManagerInternal { public static final String TAG = "BackupManagerService"; public static final boolean DEBUG = true; public static final boolean MORE_DEBUG = false; @@ -191,7 +192,6 @@ public class BackupManagerService extends IBackupManager.Stub { } } - // TODO: Remove this when we implement DI by injecting in the construtor. @VisibleForTesting Handler getBackupHandler() { return mHandler; @@ -637,51 +637,28 @@ public class BackupManagerService extends IBackupManager.Stub { } @Override - public void agentConnectedForUser(int userId, String packageName, IBinder agent) - throws RemoteException { - if (isUserReadyForBackup(userId)) { - agentConnected(userId, packageName, agent); + public void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent) { + if (!isUserReadyForBackup(userId)) { + return; } - } - @Override - public void agentConnected(String packageName, IBinder agent) throws RemoteException { - agentConnectedForUser(binderGetCallingUserId(), packageName, agent); - } - - /** - * Callback: a requested backup agent has been instantiated. This should only be called from the - * {@link ActivityManager}. - */ - public void agentConnected(@UserIdInt int userId, String packageName, IBinder agentBinder) { - UserBackupManagerService userBackupManagerService = - getServiceForUserIfCallerHasPermission(userId, "agentConnected()"); + UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission( + userId, "agentConnected()"); if (userBackupManagerService != null) { userBackupManagerService.getBackupAgentConnectionManager().agentConnected(packageName, - agentBinder); + agent); } } @Override - public void agentDisconnectedForUser(int userId, String packageName) throws RemoteException { - if (isUserReadyForBackup(userId)) { - agentDisconnected(userId, packageName); + public void agentDisconnectedForUser(String packageName, @UserIdInt int userId) { + if (!isUserReadyForBackup(userId)) { + return; } - } - @Override - public void agentDisconnected(String packageName) throws RemoteException { - agentDisconnectedForUser(binderGetCallingUserId(), packageName); - } - - /** - * Callback: a backup agent has failed to come up, or has unexpectedly quit. This should only be - * called from the {@link ActivityManager}. - */ - public void agentDisconnected(@UserIdInt int userId, String packageName) { - UserBackupManagerService userBackupManagerService = - getServiceForUserIfCallerHasPermission(userId, "agentDisconnected()"); + UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission( + userId, "agentDisconnected()"); if (userBackupManagerService != null) { userBackupManagerService.getBackupAgentConnectionManager().agentDisconnected( @@ -1688,7 +1665,7 @@ public class BackupManagerService extends IBackupManager.Stub { * @param userId User id on which the backup operation is being requested. * @param message A message to include in the exception if it is thrown. */ - void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) { + private void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) { if (binderGetCallingUserId() != userId) { mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, message); @@ -1697,6 +1674,8 @@ public class BackupManagerService extends IBackupManager.Stub { /** Implementation to receive lifecycle event callbacks for system services. */ public static class Lifecycle extends SystemService { + private final BackupManagerService mBackupManagerService; + public Lifecycle(Context context) { this(context, new BackupManagerService(context)); } @@ -1704,12 +1683,14 @@ public class BackupManagerService extends IBackupManager.Stub { @VisibleForTesting Lifecycle(Context context, BackupManagerService backupManagerService) { super(context); + mBackupManagerService = backupManagerService; sInstance = backupManagerService; + LocalServices.addService(BackupManagerInternal.class, mBackupManagerService); } @Override public void onStart() { - publishService(Context.BACKUP_SERVICE, BackupManagerService.sInstance); + publishService(Context.BACKUP_SERVICE, mBackupManagerService); } @Override @@ -1717,17 +1698,17 @@ public class BackupManagerService extends IBackupManager.Stub { // Starts the backup service for this user if backup is active for this user. Offloads // work onto the handler thread {@link #mHandlerThread} to keep unlock time low since // backup is not essential for device functioning. - sInstance.postToHandler( + mBackupManagerService.postToHandler( () -> { - sInstance.updateDefaultBackupUserIdIfNeeded(); - sInstance.startServiceForUser(user.getUserIdentifier()); - sInstance.mHasFirstUserUnlockedSinceBoot = true; + mBackupManagerService.updateDefaultBackupUserIdIfNeeded(); + mBackupManagerService.startServiceForUser(user.getUserIdentifier()); + mBackupManagerService.mHasFirstUserUnlockedSinceBoot = true; }); } @Override public void onUserStopping(@NonNull TargetUser user) { - sInstance.onStopUser(user.getUserIdentifier()); + mBackupManagerService.onStopUser(user.getUserIdentifier()); } @VisibleForTesting diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 5a198a106fc5..0603c4506cd1 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -256,7 +256,7 @@ import android.app.ServiceStartNotAllowedException; import android.app.WaitResult; import android.app.assist.ActivityId; import android.app.backup.BackupAnnotations.BackupDestination; -import android.app.backup.IBackupManager; +import android.app.backup.BackupManagerInternal; import android.app.compat.CompatChanges; import android.app.job.JobParameters; import android.app.usage.UsageEvents; @@ -4490,11 +4490,8 @@ public class ActivityManagerService extends IActivityManager.Stub final int userId = app.userId; final String packageName = app.info.packageName; mHandler.post(() -> { - try { - getBackupManager().agentDisconnectedForUser(userId, packageName); - } catch (RemoteException e) { - // Can't happen; the backup manager is local - } + LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser( + packageName, userId); }); } } else { @@ -12864,6 +12861,28 @@ public class ActivityManagerService extends IActivityManager.Stub } } + final long kernelCmaUsage = Debug.getKernelCmaUsageKb(); + if (kernelCmaUsage >= 0) { + pw.print(" Kernel CMA: "); + pw.println(stringifyKBSize(kernelCmaUsage)); + // CMA memory can be in one of the following four states: + // + // 1. Free, in which case it is accounted for as part of MemFree, which + // is already considered in the lostRAM calculation below. + // + // 2. Allocated as part of a userspace allocated, in which case it is + // already accounted for in the total PSS value that was computed. + // + // 3. Allocated for storing compressed memory (ZRAM) on Android kernels. + // This is accounted for by calculating the amount of memory ZRAM + // consumes and including it in the lostRAM calculuation. + // + // 4. Allocated by a kernel driver, in which case, it is currently not + // attributed to any term that has been derived thus far. Since the + // allocations come from a kernel driver, add it to kernelUsed. + kernelUsed += kernelCmaUsage; + } + // Note: ION/DMA-BUF heap pools are reclaimable and hence, they are included as part of // memInfo.getCachedSizeKb(). final long lostRAM = memInfo.getTotalSizeKb() @@ -13381,12 +13400,32 @@ public class ActivityManagerService extends IActivityManager.Stub proto.write(MemInfoDumpProto.CACHED_KERNEL_KB, memInfo.getCachedSizeKb()); proto.write(MemInfoDumpProto.FREE_KB, memInfo.getFreeSizeKb()); } + // CMA memory can be in one of the following four states: + // + // 1. Free, in which case it is accounted for as part of MemFree, which + // is already considered in the lostRAM calculation below. + // + // 2. Allocated as part of a userspace allocated, in which case it is + // already accounted for in the total PSS value that was computed. + // + // 3. Allocated for storing compressed memory (ZRAM) on Android Kernels. + // This is accounted for by calculating hte amount of memory ZRAM + // consumes and including it in the lostRAM calculation. + // + // 4. Allocated by a kernel driver, in which case, it is currently not + // attributed to any term that has been derived thus far, so subtract + // it from lostRAM. + long kernelCmaUsage = Debug.getKernelCmaUsageKb(); + if (kernelCmaUsage < 0) { + kernelCmaUsage = 0; + } long lostRAM = memInfo.getTotalSizeKb() - (ss[INDEX_TOTAL_PSS] - ss[INDEX_TOTAL_SWAP_PSS]) - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb() // NR_SHMEM is subtracted twice (getCachedSizeKb() and getKernelUsedSizeKb()) + memInfo.getShmemSizeKb() - - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb(); + - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb() + - kernelCmaUsage; proto.write(MemInfoDumpProto.USED_PSS_KB, ss[INDEX_TOTAL_PSS] - cachedPss); proto.write(MemInfoDumpProto.USED_KERNEL_KB, memInfo.getKernelUsedSizeKb()); proto.write(MemInfoDumpProto.LOST_RAM_KB, lostRAM); @@ -13505,11 +13544,8 @@ public class ActivityManagerService extends IActivityManager.Stub if (DEBUG_BACKUP || DEBUG_CLEANUP) Slog.d(TAG_CLEANUP, "App " + backupTarget.appInfo + " died during backup"); mHandler.post(() -> { - try { - getBackupManager().agentDisconnectedForUser(app.userId, app.info.packageName); - } catch (RemoteException e) { - // can't happen; backup manager is local - } + LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser( + app.info.packageName, app.userId); }); } @@ -14223,9 +14259,8 @@ public class ActivityManagerService extends IActivityManager.Stub final long oldIdent = Binder.clearCallingIdentity(); try { - getBackupManager().agentConnectedForUser(userId, agentPackageName, agent); - } catch (RemoteException e) { - // can't happen; the backup manager service is local + LocalServices.getService(BackupManagerInternal.class).agentConnectedForUser( + agentPackageName, userId, agent); } catch (Exception e) { Slog.w(TAG, "Exception trying to deliver BackupAgent binding: "); e.printStackTrace(); @@ -14565,7 +14600,7 @@ public class ActivityManagerService extends IActivityManager.Stub app.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION); } - app.setActiveInstrumentation(activeInstr); + mProcessStateController.setActiveInstrumentation(app, activeInstr); activeInstr.mFinished = false; activeInstr.mSourceUid = callingUid; activeInstr.mRunningProcesses.add(app); @@ -14711,7 +14746,7 @@ public class ActivityManagerService extends IActivityManager.Stub abiOverride, ZYGOTE_POLICY_FLAG_EMPTY); - app.setActiveInstrumentation(activeInstr); + mProcessStateController.setActiveInstrumentation(app, activeInstr); activeInstr.mFinished = false; activeInstr.mSourceUid = callingUid; activeInstr.mRunningProcesses.add(app); @@ -14848,7 +14883,7 @@ public class ActivityManagerService extends IActivityManager.Stub } instr.removeProcess(app); - app.setActiveInstrumentation(null); + mProcessStateController.setActiveInstrumentation(app, null); } app.mProfile.clearHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION); @@ -16617,7 +16652,7 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void onUserRemoved(@UserIdInt int userId) { + public void onUserRemoving(@UserIdInt int userId) { // Clean up any ActivityTaskManager state (by telling it the user is stopped) mAtmInternal.onUserStopped(userId); // Clean up various services by removing the user @@ -16631,6 +16666,12 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override + public void onUserRemoved(int userId) { + // Clean up UserController state + mUserController.onUserRemoved(userId); + } + + @Override public boolean startUserInBackground(final int userId) { return ActivityManagerService.this.startUserInBackground(userId); } @@ -19440,8 +19481,4 @@ public class ActivityManagerService extends IActivityManager.Stub } return token; } - - private IBackupManager getBackupManager() { - return IBackupManager.Stub.asInterface(ServiceManager.getService(Context.BACKUP_SERVICE)); - } } diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java index 6b24df4a1fa8..225c7ca2ca9e 100644 --- a/services/core/java/com/android/server/am/AppProfiler.java +++ b/services/core/java/com/android/server/am/AppProfiler.java @@ -2477,13 +2477,15 @@ public class AppProfiler { // This is the wildcard mode, where every process brought up for // the target instrumentation should be included. if (aInstr.mTargetInfo.packageName.equals(app.info.packageName)) { - app.setActiveInstrumentation(aInstr); + mService.mProcessStateController.setActiveInstrumentation(app, + aInstr); aInstr.mRunningProcesses.add(app); } } else { for (String proc : aInstr.mTargetProcesses) { if (proc.equals(app.processName)) { - app.setActiveInstrumentation(aInstr); + mService.mProcessStateController.setActiveInstrumentation(app, + aInstr); aInstr.mRunningProcesses.add(app); break; } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 9c569db99797..3abcd4e7a143 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -156,7 +156,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; -import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; @@ -3401,13 +3400,10 @@ public class OomAdjuster { } private static int getCpuCapability(ProcessRecord app, long nowUptime) { + // Note: persistent processes get all capabilities, including CPU_TIME. final UidRecord uidRec = app.getUidRecord(); if (uidRec != null && uidRec.isCurAllowListed()) { - // Process has user visible activities. - return PROCESS_CAPABILITY_CPU_TIME; - } - if (UserHandle.isCore(app.uid)) { - // Make sure all system components are not frozen. + // Process is in the power allowlist. return PROCESS_CAPABILITY_CPU_TIME; } if (app.mState.getCachedHasVisibleActivities()) { @@ -3418,6 +3414,12 @@ public class OomAdjuster { // It running a short fgs, just give it cpu time. return PROCESS_CAPABILITY_CPU_TIME; } + if (app.mReceivers.numberOfCurReceivers() > 0) { + return PROCESS_CAPABILITY_CPU_TIME; + } + if (app.hasActiveInstrumentation()) { + return PROCESS_CAPABILITY_CPU_TIME; + } // TODO(b/370817323): Populate this method with all of the reasons to keep a process // unfrozen. return 0; diff --git a/services/core/java/com/android/server/am/ProcessStateController.java b/services/core/java/com/android/server/am/ProcessStateController.java index 57899228e6ad..f44fb06727cf 100644 --- a/services/core/java/com/android/server/am/ProcessStateController.java +++ b/services/core/java/com/android/server/am/ProcessStateController.java @@ -246,12 +246,11 @@ public class ProcessStateController { } /** - * Set what sched group to grant a process due to running a broadcast. - * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast. + * Sets an active instrumentation running within the given process. */ - public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) { - // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model - throw new UnsupportedOperationException("Not implemented yet"); + public void setActiveInstrumentation(@NonNull ProcessRecord proc, + ActiveInstrumentation activeInstrumentation) { + proc.setActiveInstrumentation(activeInstrumentation); } /********************* Process Visibility State Events *********************/ @@ -587,6 +586,34 @@ public class ProcessStateController { psr.updateHasTopStartedAlmostPerceptibleServices(); } + /************************ Broadcast Receiver State Events **************************/ + /** + * Set what sched group to grant a process due to running a broadcast. + * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast. + */ + public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) { + // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Note that the process has started processing a broadcast receiver. + */ + public boolean incrementCurReceivers(@NonNull ProcessRecord app) { + // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model + // maybe used ActivityStateFlags instead. + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Note that the process has finished processing a broadcast receiver. + */ + public boolean decrementCurReceivers(@NonNull ProcessRecord app) { + // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model + // maybe used ActivityStateFlags instead. + throw new UnsupportedOperationException("Not implemented yet"); + } + /** * Builder for ProcessStateController. */ diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index ec74f60539a2..d76c04ac7f31 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -125,7 +125,6 @@ import android.view.Display; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.ObjectUtils; @@ -161,7 +160,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; -import java.util.function.Consumer; /** * Helper class for {@link ActivityManagerService} responsible for multi-user functionality. @@ -227,14 +225,6 @@ class UserController implements Handler.Callback { private static final int USER_SWITCH_CALLBACKS_TIMEOUT_MS = 5 * 1000; /** - * Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be - * called after dismissing the keyguard. - * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog()} - * and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}. - */ - private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000; - - /** * Time after last scheduleOnUserCompletedEvent() call at which USER_COMPLETED_EVENT_MSG will be * scheduled (although it may fire sooner instead). * When it fires, {@link #reportOnUserCompletedEvent} will be processed. @@ -455,11 +445,6 @@ class UserController implements Handler.Callback { public void onUserCreated(UserInfo user, Object token) { onUserAdded(user); } - - @Override - public void onUserRemoved(UserInfo user) { - UserController.this.onUserRemoved(user.id); - } }; UserController(ActivityManagerService service) { @@ -2010,7 +1995,7 @@ class UserController implements Handler.Callback { mInjector.getWindowManager().setSwitchingUser(true); // Only lock if the user has a secure keyguard PIN/Pattern/Pwd if (mInjector.getKeyguardManager().isDeviceSecure(userId)) { - // Make sure the device is locked before moving on with the user switch + Slogf.d(TAG, "Locking the device before moving on with the user switch"); mInjector.lockDeviceNowAndWaitForKeyguardShown(); } } @@ -2640,7 +2625,7 @@ class UserController implements Handler.Callback { EventLog.writeEvent(EventLogTags.UC_CONTINUE_USER_SWITCH, oldUserId, newUserId); - // Do the keyguard dismiss and dismiss the user switching dialog later + // Dismiss the user switching dialog and complete the user switch mHandler.removeMessages(COMPLETE_USER_SWITCH_MSG); mHandler.sendMessage(mHandler.obtainMessage( COMPLETE_USER_SWITCH_MSG, oldUserId, newUserId)); @@ -2655,31 +2640,17 @@ class UserController implements Handler.Callback { @VisibleForTesting void completeUserSwitch(int oldUserId, int newUserId) { - final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled(); - // serialize each conditional step - await( - // STEP 1 - If there is no challenge set, dismiss the keyguard right away - isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId), - mInjector::dismissKeyguard, - () -> await( - // STEP 2 - If user switch ui was enabled, dismiss user switch dialog - isUserSwitchUiEnabled, - this::dismissUserSwitchDialog, - () -> { - // STEP 3 - Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast - // ACTION_USER_SWITCHED & call UserSwitchObservers.onUserSwitchComplete - mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG); - mHandler.sendMessage(mHandler.obtainMessage( - REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId)); - } - )); - } - - private void await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) { - if (condition) { - conditionalStep.accept(nextStep); + final Runnable runnable = () -> { + // Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast ACTION_USER_SWITCHED and call + // onUserSwitchComplete on UserSwitchObservers. + mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG); + mHandler.sendMessage(mHandler.obtainMessage( + REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId)); + }; + if (isUserSwitchUiEnabled()) { + dismissUserSwitchDialog(runnable); } else { - nextStep.run(); + runnable.run(); } } @@ -3381,10 +3352,12 @@ class UserController implements Handler.Callback { if (mUserProfileGroupIds.keyAt(i) == userId || mUserProfileGroupIds.valueAt(i) == userId) { mUserProfileGroupIds.removeAt(i); - } } mCurrentProfileIds = ArrayUtils.removeInt(mCurrentProfileIds, userId); + mUserLru.remove((Integer) userId); + mStartedUsers.remove(userId); + updateStartedUserArrayLU(); } } @@ -4127,33 +4100,6 @@ class UserController implements Handler.Callback { return IStorageManager.Stub.asInterface(ServiceManager.getService("mount")); } - protected void dismissKeyguard(Runnable runnable) { - final AtomicBoolean isFirst = new AtomicBoolean(true); - final Runnable runOnce = () -> { - if (isFirst.getAndSet(false)) { - runnable.run(); - } - }; - - mHandler.postDelayed(runOnce, DISMISS_KEYGUARD_TIMEOUT_MS); - getWindowManager().dismissKeyguard(new IKeyguardDismissCallback.Stub() { - @Override - public void onDismissError() throws RemoteException { - mHandler.post(runOnce); - } - - @Override - public void onDismissSucceeded() throws RemoteException { - mHandler.post(runOnce); - } - - @Override - public void onDismissCancelled() throws RemoteException { - mHandler.post(runOnce); - } - }, /* message= */ null); - } - boolean isHeadlessSystemUserMode() { return UserManager.isHeadlessSystemUserMode(); } @@ -4178,6 +4124,7 @@ class UserController implements Handler.Callback { void lockDeviceNowAndWaitForKeyguardShown() { if (getWindowManager().isKeyguardLocked()) { + Slogf.w(TAG, "Not locking the device since the keyguard is already locked"); return; } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 00d23cc6d298..b9b06701a11b 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -47,6 +47,7 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL; import static android.media.AudioManager.RINGER_MODE_SILENT; import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static android.media.AudioManager.STREAM_SYSTEM; +import static android.media.IAudioManagerNative.HardeningType; import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; import static android.media.audio.Flags.concurrentAudioRecordBypassPermission; @@ -151,6 +152,7 @@ import android.media.BluetoothProfileConnectionInfo; import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; +import android.media.IAudioManagerNative; import android.media.IAudioModeDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; @@ -835,6 +837,18 @@ public class AudioService extends IAudioService.Stub private final UserRestrictionsListener mUserRestrictionsListener = new AudioServiceUserRestrictionsListener(); + private final IAudioManagerNative mNativeShim = new IAudioManagerNative.Stub() { + // oneway + @Override + public void playbackHardeningEvent(int uid, byte type, boolean bypassed) { + } + + @Override + public void permissionUpdateBarrier() { + AudioService.this.permissionUpdateBarrier(); + } + }; + // List of binder death handlers for setMode() client processes. // The last process to have called setMode() is at the top of the list. // package-private so it can be accessed in AudioDeviceBroker.getSetModeDeathHandlers @@ -2811,6 +2825,11 @@ public class AudioService extends IAudioService.Stub args, callback, resultReceiver); } + @Override + public IAudioManagerNative getNativeInterface() { + return mNativeShim; + } + /** @see AudioManager#getSurroundFormats() */ @Override public Map<Integer, Boolean> getSurroundFormats() { diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 24296406da00..93fdbc787ed0 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -316,6 +316,31 @@ final class InputGestureManager { } } + @Nullable + public InputGestureData getInputGesture(int userId, InputGestureData.Trigger trigger) { + synchronized (mGestureLock) { + if (mBlockListedTriggers.contains(trigger)) { + return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build(); + } + if (trigger instanceof InputGestureData.KeyTrigger keyTrigger) { + if (KeyEvent.isModifierKey(keyTrigger.getKeycode()) || + KeyEvent.isSystemKey(keyTrigger.getKeycode())) { + return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build(); + } + } + InputGestureData gestureData = mSystemShortcuts.get(trigger); + if (gestureData != null) { + return gestureData; + } + if (!mCustomInputGestures.contains(userId)) { + return null; + } + return mCustomInputGestures.get(userId).get(trigger); + } + } + @InputManager.CustomInputGestureResult public int addCustomInputGesture(int userId, InputGestureData newGesture) { synchronized (mGestureLock) { diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index b2c35e1f362e..2ba35d6a70d2 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -3061,6 +3061,16 @@ public class InputManagerService extends IInputManager.Stub @Override @PermissionManuallyEnforced + public AidlInputGestureData getInputGesture(@UserIdInt int userId, + @NonNull AidlInputGestureData.Trigger trigger) { + enforceManageKeyGesturePermission(); + + Objects.requireNonNull(trigger); + return mKeyGestureController.getInputGesture(userId, trigger); + } + + @Override + @PermissionManuallyEnforced public int addCustomInputGesture(@UserIdInt int userId, @NonNull AidlInputGestureData inputGestureData) { enforceManageKeyGesturePermission(); diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 5f7ad2797368..fb5ce5b4e5fa 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -18,6 +18,8 @@ package com.android.server.input; import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_WATCH; +import static android.os.UserManager.isVisibleBackgroundUsersEnabled; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE; import static com.android.hardware.input.Flags.enableNew25q2Keycodes; @@ -55,7 +57,6 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.SparseArray; -import android.view.Display; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -64,6 +65,8 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.IShortcutService; +import com.android.server.LocalServices; +import com.android.server.pm.UserManagerInternal; import com.android.server.policy.KeyCombinationManager; import java.util.ArrayDeque; @@ -159,6 +162,10 @@ final class KeyGestureController { /** Currently fully consumed key codes per device */ private final SparseArray<Set<Integer>> mConsumedKeysForDevice = new SparseArray<>(); + private final UserManagerInternal mUserManagerInternal; + + private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled(); + KeyGestureController(Context context, Looper looper, InputDataStore inputDataStore) { mContext = context; mHandler = new Handler(looper, this::handleMessage); @@ -180,6 +187,7 @@ final class KeyGestureController { mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext); mInputGestureManager = new InputGestureManager(mContext); mInputDataStore = inputDataStore; + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); initBehaviors(); initKeyCombinationRules(); } @@ -449,6 +457,9 @@ final class KeyGestureController { } public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { + if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) { + return false; + } final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0; if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures() && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { @@ -457,6 +468,24 @@ final class KeyGestureController { return false; } + private boolean shouldIgnoreKeyEventForVisibleBackgroundUser(KeyEvent event) { + final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay( + event.getDisplayId()); + final int currentUserId; + synchronized (mUserLock) { + currentUserId = mCurrentUserId; + } + if (currentUserId != displayAssignedUserId + && !KeyEvent.isVisibleBackgroundUserAllowedKey(event.getKeyCode())) { + if (DEBUG) { + Slog.w(TAG, "Ignored key event [" + event + "] for visible background user [" + + displayAssignedUserId + "]"); + } + return true; + } + return false; + } + public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event, int policyFlags) { // TODO(b/358569822): Handle shortcuts trigger logic here and pass it to appropriate @@ -895,7 +924,7 @@ final class KeyGestureController { private void handleMultiKeyGesture(int[] keycodes, @KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) { handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0, - gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags, + gestureType, action, DEFAULT_DISPLAY, /* focusedToken = */null, flags, /* appLaunchData = */null); } @@ -903,7 +932,7 @@ final class KeyGestureController { @Nullable AppLaunchData appLaunchData) { handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, new int[0], /* modifierState= */0, keyGestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, - Display.DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData); + DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData); } @VisibleForTesting @@ -915,6 +944,11 @@ final class KeyGestureController { } private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) { + if (mVisibleBackgroundUsersEnabled && event.displayId != DEFAULT_DISPLAY + && shouldIgnoreGestureEventForVisibleBackgroundUser(event.gestureType, + event.displayId)) { + return false; + } synchronized (mKeyGestureHandlerRecords) { for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { if (handler.handleKeyGesture(event, focusedToken)) { @@ -927,6 +961,24 @@ final class KeyGestureController { return false; } + private boolean shouldIgnoreGestureEventForVisibleBackgroundUser( + @KeyGestureEvent.KeyGestureType int gestureType, int displayId) { + final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay(displayId); + final int currentUserId; + synchronized (mUserLock) { + currentUserId = mCurrentUserId; + } + if (currentUserId != displayAssignedUserId + && !KeyGestureEvent.isVisibleBackgrounduserAllowedGesture(gestureType)) { + if (DEBUG) { + Slog.w(TAG, "Ignored gesture event [" + gestureType + + "] for visible background user [" + displayAssignedUserId + "]"); + } + return true; + } + return false; + } + private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) { synchronized (mKeyGestureHandlerRecords) { for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { @@ -943,7 +995,7 @@ final class KeyGestureController { // TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally // should not rely on PWM to tell us about the gesture start and end. AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState, - gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, + gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY, /* flags = */0, /* appLaunchData = */null); mHandler.obtainMessage(MSG_NOTIFY_KEY_GESTURE_EVENT, event).sendToTarget(); } @@ -951,7 +1003,7 @@ final class KeyGestureController { public void handleKeyGesture(int deviceId, int[] keycodes, int modifierState, @KeyGestureEvent.KeyGestureType int gestureType) { AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState, - gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, + gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY, /* flags = */0, /* appLaunchData = */null); handleKeyGesture(event, null /*focusedToken*/); } @@ -1069,6 +1121,18 @@ final class KeyGestureController { } @BinderThread + @Nullable + public AidlInputGestureData getInputGesture(@UserIdInt int userId, + @NonNull AidlInputGestureData.Trigger trigger) { + InputGestureData gestureData = mInputGestureManager.getInputGesture(userId, + InputGestureData.createTriggerFromAidlTrigger(trigger)); + if (gestureData == null) { + return null; + } + return gestureData.getAidlData(); + } + + @BinderThread @InputManager.CustomInputGestureResult public int addCustomInputGesture(@UserIdInt int userId, @NonNull AidlInputGestureData inputGestureData) { diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java index b0dff22c6f03..281db0ae9518 100644 --- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -387,8 +387,9 @@ public final class ImeVisibilityStateComputer { @GuardedBy("ImfLock.class") void setWindowState(IBinder windowToken, @NonNull ImeTargetWindowState newState) { final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); - if (state != null && newState.hasEditorFocused() - && newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS) { + if (state != null && newState.hasEditorFocused() && ( + newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS + || Flags.refactorInsetsController())) { // Inherit the last requested IME visible state when the target window is still // focused with an editor. newState.setRequestedImeVisible(state.mRequestedImeVisible); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index f5ed4d586131..1e54beeb2d64 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -248,6 +248,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } mEndpointManager.unregisterEndpoint(mEndpointInfo.getIdentifier().getEndpoint()); + releaseWakeLockOnExit(); } @Override @@ -420,6 +421,25 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } /* package */ void onMessageReceived(int sessionId, HubMessage message) { + byte code = onMessageReceivedInternal(sessionId, message); + if (code != ErrorCode.OK && message.isResponseRequired()) { + sendMessageDeliveryStatus( + sessionId, message.getMessageSequenceNumber(), code); + } + } + + /* package */ void onMessageDeliveryStatusReceived( + int sessionId, int sequenceNumber, byte errorCode) { + mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK); + } + + /* package */ boolean hasSessionId(int sessionId) { + synchronized (mOpenSessionLock) { + return mSessionInfoMap.contains(sessionId); + } + } + + private byte onMessageReceivedInternal(int sessionId, HubMessage message) { HubEndpointInfo remote; synchronized (mOpenSessionLock) { if (!isSessionActive(sessionId)) { @@ -429,9 +449,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + sessionId + ") with message: " + message); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMANENT_ERROR); - return; + return ErrorCode.PERMANENT_ERROR; } remote = mSessionInfoMap.get(sessionId).getRemoteEndpointInfo(); } @@ -453,28 +471,12 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + ". " + mPackageName + " doesn't have permission"); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMISSION_DENIED); - return; + return ErrorCode.PERMISSION_DENIED; } boolean success = invokeCallback((consumer) -> consumer.onMessageReceived(sessionId, message)); - if (!success) { - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.TRANSIENT_ERROR); - } - } - - /* package */ void onMessageDeliveryStatusReceived( - int sessionId, int sequenceNumber, byte errorCode) { - mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK); - } - - /* package */ boolean hasSessionId(int sessionId) { - synchronized (mOpenSessionLock) { - return mSessionInfoMap.contains(sessionId); - } + return success ? ErrorCode.OK : ErrorCode.TRANSIENT_ERROR; } /** @@ -558,6 +560,23 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub }); } + private void releaseWakeLockOnExit() { + Binder.withCleanCallingIdentity( + () -> { + while (mWakeLock.isHeld()) { + try { + mWakeLock.release(); + } catch (RuntimeException e) { + Log.e( + TAG, + "Releasing the wakelock for all acquisitions fails - ", + e); + break; + } + } + }); + } + /** * Invokes a callback and acquires a wakelock. * diff --git a/services/core/java/com/android/server/location/fudger/LocationFudger.java b/services/core/java/com/android/server/location/fudger/LocationFudger.java index 27577641ad1d..28e21b71dcc9 100644 --- a/services/core/java/com/android/server/location/fudger/LocationFudger.java +++ b/services/core/java/com/android/server/location/fudger/LocationFudger.java @@ -302,6 +302,15 @@ public class LocationFudger { // requires latitude since longitudinal distances change with distance from equator. private static double metersToDegreesLongitude(double distance, double lat) { - return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat)); + // Needed to convert from longitude distance to longitude degree. + // X meters near the poles is more degrees than at the equator. + double cosLat = Math.cos(Math.toRadians(lat)); + // If we are right on top of the pole, the degree is always 0. + // We return a very small value instead to avoid divide by zero errors + // later on. + if (cosLat == 0.0) { + return 0.0001; + } + return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / cosLat; } } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 4cb776924ca4..a0fbc008475c 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -125,7 +125,6 @@ import android.content.pm.SharedLibraryInfo; import android.content.pm.Signature; import android.content.pm.SigningDetails; import android.content.pm.VerifierInfo; -import android.content.pm.dex.DexMetadataHelper; import android.content.pm.parsing.result.ParseResult; import android.content.pm.parsing.result.ParseTypeImpl; import android.net.Uri; @@ -171,7 +170,6 @@ import com.android.internal.pm.pkg.component.ParsedIntentInfo; import com.android.internal.pm.pkg.component.ParsedPermission; import com.android.internal.pm.pkg.component.ParsedPermissionGroup; import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; -import com.android.internal.security.VerityUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; import com.android.server.EventLogTags; @@ -186,7 +184,6 @@ import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.pkg.SharedLibraryWrapper; import com.android.server.rollback.RollbackManagerInternal; -import com.android.server.security.FileIntegrityService; import com.android.server.utils.WatchedArrayMap; import com.android.server.utils.WatchedLongSparseArray; @@ -195,7 +192,6 @@ import dalvik.system.VMRuntime; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.security.DigestException; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -1165,11 +1161,8 @@ final class InstallPackageHelper { } try { doRenameLI(request, parsedPackage); - setUpFsVerity(parsedPackage); - } catch (Installer.InstallerException | IOException | DigestException - | NoSuchAlgorithmException | PrepareFailure e) { - request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP, - "Failed to set up verity: " + e); + } catch (PrepareFailure e) { + request.setError(e); return false; } @@ -2322,68 +2315,6 @@ final class InstallPackageHelper { } } - /** - * Set up fs-verity for the given package. For older devices that do not support fs-verity, - * this is a no-op. - */ - private void setUpFsVerity(AndroidPackage pkg) throws Installer.InstallerException, - PrepareFailure, IOException, DigestException, NoSuchAlgorithmException { - if (!PackageManagerServiceUtils.isApkVerityEnabled()) { - return; - } - - if (isIncrementalPath(pkg.getPath()) && IncrementalManager.getVersion() - < IncrementalManager.MIN_VERSION_TO_SUPPORT_FSVERITY) { - return; - } - - // Collect files we care for fs-verity setup. - ArrayMap<String, String> fsverityCandidates = new ArrayMap<>(); - fsverityCandidates.put(pkg.getBaseApkPath(), - VerityUtils.getFsveritySignatureFilePath(pkg.getBaseApkPath())); - - final String dmPath = DexMetadataHelper.buildDexMetadataPathForApk( - pkg.getBaseApkPath()); - if (new File(dmPath).exists()) { - fsverityCandidates.put(dmPath, VerityUtils.getFsveritySignatureFilePath(dmPath)); - } - - for (String path : pkg.getSplitCodePaths()) { - fsverityCandidates.put(path, VerityUtils.getFsveritySignatureFilePath(path)); - - final String splitDmPath = DexMetadataHelper.buildDexMetadataPathForApk(path); - if (new File(splitDmPath).exists()) { - fsverityCandidates.put(splitDmPath, - VerityUtils.getFsveritySignatureFilePath(splitDmPath)); - } - } - - var fis = FileIntegrityService.getService(); - for (Map.Entry<String, String> entry : fsverityCandidates.entrySet()) { - try { - final String filePath = entry.getKey(); - if (VerityUtils.hasFsverity(filePath)) { - continue; - } - - final String signaturePath = entry.getValue(); - if (new File(signaturePath).exists()) { - // If signature is provided, enable fs-verity first so that the file can be - // measured for signature check below. - VerityUtils.setUpFsverity(filePath); - - if (!fis.verifyPkcs7DetachedSignature(signaturePath, filePath)) { - throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE, - "fs-verity signature does not verify against a known key"); - } - } - } catch (IOException e) { - throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE, - "Failed to enable fs-verity: " + e); - } - } - } - private PackageFreezer freezePackageForInstall(String packageName, int userId, int installFlags, String killReason, int exitInfoReason, InstallRequest request) { if ((installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) { diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index c6760431116e..1b41c3617a05 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -26,7 +26,6 @@ import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH; import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED; -import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE; import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR; import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; @@ -824,8 +823,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private File mInheritedFilesBase; - @GuardedBy("mLock") - private boolean mVerityFoundForApks; /** * Both flags should be guarded with mLock whenever changes need to be in lockstep. @@ -864,7 +861,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } else { if (DexMetadataHelper.isDexMetadataFile(file)) return false; } - if (VerityUtils.isFsveritySignatureFile(file)) return false; if (ApkChecksums.isDigestOrDigestSignatureFile(file)) return false; return true; } @@ -3565,13 +3561,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { "Missing existing base package"); } - // Default to require only if existing base apk has fs-verity signature. - mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled() - && params.mode == SessionParams.MODE_INHERIT_EXISTING - && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath()) - && (new File(VerityUtils.getFsveritySignatureFilePath( - pkgInfo.applicationInfo.getBaseCodePath()))).exists(); - final List<File> removedFiles = getRemovedFilesLocked(); final List<String> removeSplitList = new ArrayList<>(); if (!removedFiles.isEmpty()) { @@ -3972,24 +3961,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private void maybeStageFsveritySignatureLocked(File origFile, File targetFile, - boolean fsVerityRequired) throws PackageManagerException { - if (android.security.Flags.deprecateFsvSig()) { - return; - } - final File originalSignature = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (originalSignature.exists()) { - final File stagedSignature = new File( - VerityUtils.getFsveritySignatureFilePath(targetFile.getPath())); - stageFileLocked(originalSignature, stagedSignature); - } else if (fsVerityRequired) { - throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE, - "Missing corresponding fs-verity signature to " + origFile); - } - } - - @GuardedBy("mLock") private void maybeStageV4SignatureLocked(File origFile, File targetFile) throws PackageManagerException { final File originalSignature = new File(origFile.getPath() + V4Signature.EXT); @@ -4015,11 +3986,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { DexMetadataHelper.buildDexMetadataPathForApk(targetFile.getName())); stageFileLocked(dexMetadataFile, targetDexMetadataFile); - - // Also stage .dm.fsv_sig. .dm may be required to install with fs-verity signature on - // supported on older devices. - maybeStageFsveritySignatureLocked(dexMetadataFile, targetDexMetadataFile, - DexMetadataHelper.isFsVerityRequired()); } @FlaggedApi(com.android.art.flags.Flags.FLAG_ART_SERVICE_V3) @@ -4105,44 +4071,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private boolean isFsVerityRequiredForApk(File origFile, File targetFile) - throws PackageManagerException { - if (mVerityFoundForApks) { - return true; - } - - // We haven't seen .fsv_sig for any APKs. Treat it as not required until we see one. - final File originalSignature = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (!originalSignature.exists()) { - return false; - } - mVerityFoundForApks = true; - - // When a signature is found, also check any previous staged APKs since they also need to - // have fs-verity signature consistently. - for (File file : mResolvedStagedFiles) { - if (!file.getName().endsWith(".apk")) { - continue; - } - // Ignore the current targeting file. - if (targetFile.getName().equals(file.getName())) { - continue; - } - throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE, - "Previously staged apk is missing fs-verity signature"); - } - return true; - } - - @GuardedBy("mLock") private void resolveAndStageFileLocked(File origFile, File targetFile, String splitName, List<String> artManagedFilePaths) throws PackageManagerException { stageFileLocked(origFile, targetFile); - // Stage APK's fs-verity signature if present. - maybeStageFsveritySignatureLocked(origFile, targetFile, - isFsVerityRequiredForApk(origFile, targetFile)); // Stage APK's v4 signature if present, and fs-verity is supported. if (android.security.Flags.extendVbChainToUpdatedApk() && VerityUtils.isFsVeritySupported()) { @@ -4160,16 +4092,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private void maybeInheritFsveritySignatureLocked(File origFile) { - // Inherit the fsverity signature file if present. - final File fsveritySignatureFile = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (fsveritySignatureFile.exists()) { - mResolvedInheritedFiles.add(fsveritySignatureFile); - } - } - - @GuardedBy("mLock") private void maybeInheritV4SignatureLocked(File origFile) { // Inherit the v4 signature file if present. final File v4SignatureFile = new File(origFile.getPath() + V4Signature.EXT); @@ -4182,7 +4104,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private void inheritFileLocked(File origFile, List<String> artManagedFilePaths) { mResolvedInheritedFiles.add(origFile); - maybeInheritFsveritySignatureLocked(origFile); if (android.security.Flags.extendVbChainToUpdatedApk()) { maybeInheritV4SignatureLocked(origFile); } @@ -4193,13 +4114,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { artManagedFilePaths, origFile.getPath())) { File artManagedFile = new File(path); mResolvedInheritedFiles.add(artManagedFile); - maybeInheritFsveritySignatureLocked(artManagedFile); } } else { final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(origFile); if (dexMetadataFile != null) { mResolvedInheritedFiles.add(dexMetadataFile); - maybeInheritFsveritySignatureLocked(dexMetadataFile); } } // Inherit the digests if present. diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 7af39f74d0d6..3e376b6958ec 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -520,22 +520,6 @@ public class PackageManagerServiceUtils { } } - /** Default is to not use fs-verity since it depends on kernel support. */ - private static final int FSVERITY_DISABLED = 0; - - /** Standard fs-verity. */ - private static final int FSVERITY_ENABLED = 2; - - /** Returns true if standard APK Verity is enabled. */ - static boolean isApkVerityEnabled() { - if (android.security.Flags.deprecateFsvSig()) { - return false; - } - return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R - || SystemProperties.getInt("ro.apk_verity.mode", FSVERITY_DISABLED) - == FSVERITY_ENABLED; - } - /** * Verifies that signatures match. * @returns {@code true} if the compat signatures were matched; otherwise, {@code false}. diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 8249d65868cd..81956fbb55e6 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -6661,7 +6661,7 @@ public class UserManagerService extends IUserManager.Stub { + userId); } new Thread(() -> { - getActivityManagerInternal().onUserRemoved(userId); + getActivityManagerInternal().onUserRemoving(userId); removeUserState(userId); }).start(); } @@ -6701,6 +6701,7 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mUsersLock) { removeUserDataLU(userId); mIsUserManaged.delete(userId); + getActivityManagerInternal().onUserRemoved(userId); } synchronized (mUserStates) { mUserStates.delete(userId); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 23383a9c55c0..f9e4022f04a0 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1395,7 +1395,9 @@ public final class PowerManagerService extends SystemService DisplayGroupPowerChangeListener displayGroupPowerChangeListener = new DisplayGroupPowerChangeListener(); mDisplayManagerInternal.registerDisplayGroupListener(displayGroupPowerChangeListener); - mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler); + if (mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler); + } if(mDreamManager != null){ // This DreamManager method does not acquire a lock, so it should be safe to call. @@ -3852,6 +3854,10 @@ public final class PowerManagerService extends SystemService @GuardedBy("mLock") private void notifyScreenTimeoutPolicyChangesLocked() { + if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + return; + } + for (int idx = 0; idx < mPowerGroups.size(); idx++) { final int powerGroupId = mPowerGroups.keyAt(idx); final PowerGroup powerGroup = mPowerGroups.valueAt(idx); @@ -6011,6 +6017,11 @@ public final class PowerManagerService extends SystemService @Override // Binder call public void addScreenTimeoutPolicyListener(int displayId, IScreenTimeoutPolicyListener listener) { + if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + throw new IllegalStateException("Screen timeout policy listener API flag " + + "is not enabled"); + } + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null); @@ -6042,6 +6053,11 @@ public final class PowerManagerService extends SystemService @Override // Binder call public void removeScreenTimeoutPolicyListener(int displayId, IScreenTimeoutPolicyListener listener) { + if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + throw new IllegalStateException("Screen timeout policy listener API flag " + + "is not enabled"); + } + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null); diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java index 42dbb7974fe2..f46fa446a0ba 100644 --- a/services/core/java/com/android/server/power/ThermalManagerService.java +++ b/services/core/java/com/android/server/power/ThermalManagerService.java @@ -155,6 +155,9 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting final TemperatureWatcher mTemperatureWatcher; + @VisibleForTesting + final AtomicBoolean mIsHalSkinForecastSupported = new AtomicBoolean(false); + private final ThermalHalWrapper.WrapperThermalChangedCallback mWrapperCallback = new ThermalHalWrapper.WrapperThermalChangedCallback() { @Override @@ -254,6 +257,18 @@ public class ThermalManagerService extends SystemService { } onTemperatureMapChangedLocked(); mTemperatureWatcher.getAndUpdateThresholds(); + // we only check forecast if a single SKIN sensor threshold is reported + synchronized (mTemperatureWatcher.mSamples) { + if (mTemperatureWatcher.mSevereThresholds.size() == 1) { + try { + mIsHalSkinForecastSupported.set( + Flags.allowThermalHalSkinForecast() + && !Float.isNaN(mHalWrapper.forecastSkinTemperature(10))); + } catch (UnsupportedOperationException e) { + Slog.i(TAG, "Thermal HAL does not support forecastSkinTemperature"); + } + } + } mHalReady.set(true); } } @@ -1092,6 +1107,8 @@ public class ThermalManagerService extends SystemService { protected abstract List<TemperatureThreshold> getTemperatureThresholds(boolean shouldFilter, int type); + protected abstract float forecastSkinTemperature(int forecastSeconds); + protected abstract boolean connectToHal(); protected abstract void dump(PrintWriter pw, String prefix); @@ -1124,8 +1141,16 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting static class ThermalHalAidlWrapper extends ThermalHalWrapper implements IBinder.DeathRecipient { /* Proxy object for the Thermal HAL AIDL service. */ + + @GuardedBy("mHalLock") private IThermal mInstance = null; + private IThermal getHalInstance() { + synchronized (mHalLock) { + return mInstance; + } + } + /** Callback for Thermal HAL AIDL. */ private final IThermalChangedCallback mThermalCallbackAidl = new IThermalChangedCallback.Stub() { @@ -1169,154 +1194,183 @@ public class ThermalManagerService extends SystemService { @Override protected List<Temperature> getCurrentTemperatures(boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<Temperature> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<Temperature> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final android.hardware.thermal.Temperature[] halRet = + shouldFilter ? instance.getTemperaturesWithType(type) + : instance.getTemperatures(); + if (halRet == null) { return ret; } - try { - final android.hardware.thermal.Temperature[] halRet = - shouldFilter ? mInstance.getTemperaturesWithType(type) - : mInstance.getTemperatures(); - if (halRet == null) { - return ret; + for (android.hardware.thermal.Temperature t : halRet) { + if (!Temperature.isValidStatus(t.throttlingStatus)) { + Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus + + " received from AIDL HAL"); + t.throttlingStatus = Temperature.THROTTLING_NONE; } - for (android.hardware.thermal.Temperature t : halRet) { - if (!Temperature.isValidStatus(t.throttlingStatus)) { - Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus - + " received from AIDL HAL"); - t.throttlingStatus = Temperature.THROTTLING_NONE; - } - if (shouldFilter && t.type != type) { - continue; - } - ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus)); + if (shouldFilter && t.type != type) { + continue; } - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e); - connectToHal(); + ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus)); + } + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; } @Override protected List<CoolingDevice> getCurrentCoolingDevices(boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<CoolingDevice> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<CoolingDevice> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter + ? instance.getCoolingDevicesWithType(type) + : instance.getCoolingDevices(); + if (halRet == null) { return ret; } - try { - final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter - ? mInstance.getCoolingDevicesWithType(type) - : mInstance.getCoolingDevices(); - if (halRet == null) { - return ret; + for (android.hardware.thermal.CoolingDevice t : halRet) { + if (!CoolingDevice.isValidType(t.type)) { + Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL"); + continue; } - for (android.hardware.thermal.CoolingDevice t : halRet) { - if (!CoolingDevice.isValidType(t.type)) { - Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL"); - continue; - } - if (shouldFilter && t.type != type) { - continue; - } - ret.add(new CoolingDevice(t.value, t.type, t.name)); + if (shouldFilter && t.type != type) { + continue; } - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e); - connectToHal(); + ret.add(new CoolingDevice(t.value, t.type, t.name)); + } + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; } @Override @NonNull protected List<TemperatureThreshold> getTemperatureThresholds( boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<TemperatureThreshold> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<TemperatureThreshold> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final TemperatureThreshold[] halRet = + shouldFilter ? instance.getTemperatureThresholdsWithType(type) + : instance.getTemperatureThresholds(); + if (halRet == null) { return ret; } - try { - final TemperatureThreshold[] halRet = - shouldFilter ? mInstance.getTemperatureThresholdsWithType(type) - : mInstance.getTemperatureThresholds(); - if (halRet == null) { - return ret; - } - if (shouldFilter) { - return Arrays.stream(halRet).filter(t -> t.type == type).collect( - Collectors.toList()); - } - return Arrays.asList(halRet); - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e); - connectToHal(); + if (shouldFilter) { + return Arrays.stream(halRet).filter(t -> t.type == type).collect( + Collectors.toList()); + } + return Arrays.asList(halRet); + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; + } + + @Override + protected float forecastSkinTemperature(int forecastSeconds) { + final IThermal instance = getHalInstance(); + if (instance == null) { + return Float.NaN; + } + try { + return instance.forecastSkinTemperature(forecastSeconds); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't forecastSkinTemperature, reconnecting...", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); + } + } + return Float.NaN; } @Override protected boolean connectToHal() { synchronized (mHalLock) { - IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService( - IThermal.DESCRIPTOR + "/default")); - initProxyAndRegisterCallback(binder); + return connectToHalIfNeededLocked(mInstance); } + } + + @GuardedBy("mHalLock") + protected boolean connectToHalIfNeededLocked(IThermal instance) { + if (instance != mInstance) { + // instance has been updated since last used + return true; + } + IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService( + IThermal.DESCRIPTOR + "/default")); + initProxyAndRegisterCallbackLocked(binder); return mInstance != null; } @VisibleForTesting void initProxyAndRegisterCallback(IBinder binder) { synchronized (mHalLock) { - if (binder != null) { - mInstance = IThermal.Stub.asInterface(binder); + initProxyAndRegisterCallbackLocked(binder); + } + } + + @GuardedBy("mHalLock") + protected void initProxyAndRegisterCallbackLocked(IBinder binder) { + if (binder != null) { + mInstance = IThermal.Stub.asInterface(binder); + try { + binder.linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + connectToHal(); + } + if (mInstance != null) { try { - binder.linkToDeath(this, 0); + Slog.i(TAG, "Thermal HAL AIDL service connected with version " + + mInstance.getInterfaceVersion()); } catch (RemoteException e) { - Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + Slog.e(TAG, "Unable to read interface version from Thermal HAL", e); connectToHal(); + return; } - if (mInstance != null) { - try { - Slog.i(TAG, "Thermal HAL AIDL service connected with version " - + mInstance.getInterfaceVersion()); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to read interface version from Thermal HAL", e); - connectToHal(); - return; - } - registerThermalChangedCallback(); + try { + mInstance.registerThermalChangedCallback(mThermalCallbackAidl); + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status", + e); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + connectToHal(); } } } } - @VisibleForTesting - void registerThermalChangedCallback() { - try { - mInstance.registerThermalChangedCallback(mThermalCallbackAidl); - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status", - e); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); - connectToHal(); - } - } - @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { @@ -1445,6 +1499,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 1.0"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1583,6 +1642,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 1.1"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1749,6 +1813,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 2.0"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1977,6 +2046,39 @@ public class ThermalManagerService extends SystemService { float getForecast(int forecastSeconds) { synchronized (mSamples) { + // If we don't have any thresholds, we can't normalize the temperatures, + // so return early + if (mSevereThresholds.isEmpty()) { + Slog.e(TAG, "No temperature thresholds found"); + FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, + Binder.getCallingUid(), + THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD, + Float.NaN, forecastSeconds); + return Float.NaN; + } + } + if (mIsHalSkinForecastSupported.get()) { + float threshold = -1f; + synchronized (mSamples) { + // we only do forecast if a single SKIN sensor threshold is reported + if (mSevereThresholds.size() == 1) { + threshold = mSevereThresholds.valueAt(0); + } + } + if (threshold > 0) { + try { + final float forecastTemperature = + mHalWrapper.forecastSkinTemperature(forecastSeconds); + return normalizeTemperature(forecastTemperature, threshold); + } catch (UnsupportedOperationException e) { + Slog.wtf(TAG, "forecastSkinTemperature returns unsupported"); + } catch (Exception e) { + Slog.e(TAG, "forecastSkinTemperature fails"); + } + return Float.NaN; + } + } + synchronized (mSamples) { mLastForecastCallTimeMillis = SystemClock.elapsedRealtime(); if (mSamples.isEmpty()) { getAndUpdateTemperatureSamples(); @@ -1993,17 +2095,6 @@ public class ThermalManagerService extends SystemService { return Float.NaN; } - // If we don't have any thresholds, we can't normalize the temperatures, - // so return early - if (mSevereThresholds.isEmpty()) { - Slog.e(TAG, "No temperature thresholds found"); - FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, - Binder.getCallingUid(), - THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD, - Float.NaN, forecastSeconds); - return Float.NaN; - } - if (mCachedHeadrooms.contains(forecastSeconds)) { // TODO(b/360486877): replace with metrics Slog.d(TAG, diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java index 5cd7dee35e5f..42b44013bea2 100644 --- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java +++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java @@ -37,6 +37,11 @@ public class PowerManagerFlags { Flags.FLAG_ENABLE_EARLY_SCREEN_TIMEOUT_DETECTOR, Flags::enableEarlyScreenTimeoutDetector); + private final FlagState mEnableScreenTimeoutPolicyListenerApi = new FlagState( + Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API, + Flags::enableScreenTimeoutPolicyListenerApi + ); + private final FlagState mImproveWakelockLatency = new FlagState( Flags.FLAG_IMPROVE_WAKELOCK_LATENCY, Flags::improveWakelockLatency @@ -63,6 +68,11 @@ public class PowerManagerFlags { return mEarlyScreenTimeoutDetectorFlagState.isEnabled(); } + /** Returns whether screen timeout policy listener APIs are enabled on not. */ + public boolean isScreenTimeoutPolicyListenerApiEnabled() { + return mEnableScreenTimeoutPolicyListenerApi.isEnabled(); + } + /** * @return Whether to improve the wakelock acquire/release latency or not */ diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig index a975da32f2fd..613daf820e34 100644 --- a/services/core/java/com/android/server/power/feature/power_flags.aconfig +++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig @@ -12,6 +12,17 @@ flag { } flag { + name: "enable_screen_timeout_policy_listener_api" + namespace: "power" + description: "Enables APIs that allow to listen to screen timeout policy changes" + bug: "363174979" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "improve_wakelock_latency" namespace: "power" description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock." diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java index d69150d88e4f..a1f72be7a039 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java @@ -15,7 +15,6 @@ */ package com.android.server.selinux; -import android.provider.DeviceConfig; import android.text.TextUtils; import android.util.Slog; @@ -34,10 +33,6 @@ class SelinuxAuditLogBuilder { private static final String TAG = "SelinuxAuditLogs"; - // This config indicates which Selinux logs for source domains to collect. The string will be - // inserted into a regex, so it must follow the regex syntax. For example, a valid value would - // be "system_server|untrusted_app". - @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain"; private static final Matcher NO_OP_MATCHER = Pattern.compile("no-op^").matcher(""); private static final String TCONTEXT_PATTERN = "u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*"; @@ -50,7 +45,7 @@ class SelinuxAuditLogBuilder { private Iterator<String> mTokens; private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog(); - SelinuxAuditLogBuilder() { + SelinuxAuditLogBuilder(String auditDomain) { Matcher scontextMatcher = NO_OP_MATCHER; Matcher tcontextMatcher = NO_OP_MATCHER; Matcher pathMatcher = NO_OP_MATCHER; @@ -59,10 +54,7 @@ class SelinuxAuditLogBuilder { Pattern.compile( TextUtils.formatSimple( "u:r:(?<stype>%s):s0(:c)?(?<scategories>((,c)?\\d+)+)*", - DeviceConfig.getString( - DeviceConfig.NAMESPACE_ADSERVICES, - CONFIG_SELINUX_AUDIT_DOMAIN, - "no_match^"))) + auditDomain)) .matcher(""); tcontextMatcher = Pattern.compile(TCONTEXT_PATTERN).matcher(""); pathMatcher = Pattern.compile(PATH_PATTERN).matcher(""); diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java index c655d46eb9f4..0aa705892376 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java @@ -15,6 +15,7 @@ */ package com.android.server.selinux; +import android.provider.DeviceConfig; import android.util.EventLog; import android.util.EventLog.Event; import android.util.Log; @@ -32,6 +33,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,9 +45,16 @@ class SelinuxAuditLogsCollector { private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$"; + // This config indicates which Selinux logs for source domains to collect. The string will be + // inserted into a regex, so it must follow the regex syntax. For example, a valid value would + // be "system_server|untrusted_app". + @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain"; + @VisibleForTesting static final String DEFAULT_SELINUX_AUDIT_DOMAIN = "no_match^"; + @VisibleForTesting static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher(""); + private final Supplier<String> mAuditDomainSupplier; private final RateLimiter mRateLimiter; private final QuotaLimiter mQuotaLimiter; @@ -53,11 +62,26 @@ class SelinuxAuditLogsCollector { AtomicBoolean mStopRequested = new AtomicBoolean(false); - SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + SelinuxAuditLogsCollector( + Supplier<String> auditDomainSupplier, + RateLimiter rateLimiter, + QuotaLimiter quotaLimiter) { + mAuditDomainSupplier = auditDomainSupplier; mRateLimiter = rateLimiter; mQuotaLimiter = quotaLimiter; } + SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + this( + () -> + DeviceConfig.getString( + DeviceConfig.NAMESPACE_ADSERVICES, + CONFIG_SELINUX_AUDIT_DOMAIN, + DEFAULT_SELINUX_AUDIT_DOMAIN), + rateLimiter, + quotaLimiter); + } + public void setStopRequested(boolean stopRequested) { mStopRequested.set(stopRequested); } @@ -108,7 +132,8 @@ class SelinuxAuditLogsCollector { } private boolean writeAuditLogs(Queue<Event> logLines) { - final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder(); + final SelinuxAuditLogBuilder auditLogBuilder = + new SelinuxAuditLogBuilder(mAuditDomainSupplier.get()); int auditsWritten = 0; while (!mStopRequested.get() && !logLines.isEmpty()) { diff --git a/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java new file mode 100644 index 000000000000..2e73829ca143 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java @@ -0,0 +1,649 @@ +/* + * Copyright (C) 2024 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.timezonedetector; + +import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; +import static android.app.PendingIntent.FLAG_IMMUTABLE; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static android.content.Context.RECEIVER_NOT_EXPORTED; +import static android.provider.Settings.ACTION_DATE_SETTINGS; + +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_UNKNOWN; + +import android.annotation.DurationMillisLong; +import android.annotation.IntDef; +import android.annotation.RequiresPermission; +import android.annotation.UserIdInt; +import android.app.ActivityManagerInternal; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.icu.text.DateFormat; +import android.icu.text.SimpleDateFormat; +import android.icu.util.TimeZone; +import android.os.Handler; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.IndentingPrintWriter; +import android.util.Log; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.notification.SystemNotificationChannels; +import com.android.server.LocalServices; +import com.android.server.flags.Flags; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An implementation of {@link TimeZoneChangeListener} that fires notifications. + */ +public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { + @IntDef({STATUS_UNKNOWN, STATUS_UNTRACKED, STATUS_REJECTED, + STATUS_ACCEPTED, STATUS_SUPERSEDED}) + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) + @interface TimeZoneChangeStatus {} + + /** Used to indicate the status could not be inferred. */ + @TimeZoneChangeStatus + static final int STATUS_UNKNOWN = 0; + /** Used to indicate the change is not one that needs to be tracked. */ + @TimeZoneChangeStatus + static final int STATUS_UNTRACKED = 1; + @TimeZoneChangeStatus + static final int STATUS_REJECTED = 2; + @TimeZoneChangeStatus + static final int STATUS_ACCEPTED = 3; + /** Used to indicate a change was superseded before its status could be determined. */ + @TimeZoneChangeStatus + static final int STATUS_SUPERSEDED = 4; + + @IntDef({SIGNAL_TYPE_UNKNOWN, SIGNAL_TYPE_NONE, SIGNAL_TYPE_NOTIFICATION, + SIGNAL_TYPE_HEURISTIC}) + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) + @interface SignalType {} + + /** Used when the signal type cannot be inferred. */ + @SignalType + static final int SIGNAL_TYPE_UNKNOWN = 0; + /** Used when the status is not one that needs a signal type. */ + @SignalType + static final int SIGNAL_TYPE_NONE = 1; + @SignalType + static final int SIGNAL_TYPE_NOTIFICATION = 2; + @SignalType + static final int SIGNAL_TYPE_HEURISTIC = 3; + + private static final int MAX_EVENTS_TO_TRACK = 10; + + @VisibleForTesting + @DurationMillisLong + static final long AUTO_REVERT_THRESHOLD = Duration.ofMinutes(15).toMillis(); + + private static final String TAG = "TimeZoneChangeTracker"; + private static final String NOTIFICATION_TAG = "TimeZoneDetector"; + private static final int TZ_CHANGE_NOTIFICATION_ID = 1001; + + private static final String ACTION_NOTIFICATION_DELETED = + "com.android.server.timezonedetector.TimeZoneNotificationDeleted"; + + private static final String NOTIFICATION_INTENT_EXTRA_USER_ID = "user_id"; + private static final String NOTIFICATION_INTENT_EXTRA_CHANGE_ID = "change_id"; + + private final Context mContext; + private final NotificationManager mNotificationManager; + private final ActivityManagerInternal mActivityManagerInternal; + + // For scheduling callbacks + private final Handler mHandler; + private final ServiceConfigAccessor mServiceConfigAccessor; + private final AtomicInteger mNextChangeEventId = new AtomicInteger(1); + + private final Resources mRes = Resources.getSystem(); + + @GuardedBy("mTimeZoneChangeRecord") + private final ReferenceWithHistory<TimeZoneChangeRecord> mTimeZoneChangeRecord = + new ReferenceWithHistory<>(MAX_EVENTS_TO_TRACK); + + private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case ACTION_NOTIFICATION_DELETED: + int notifiedUserId = intent.getIntExtra( + NOTIFICATION_INTENT_EXTRA_USER_ID, UserHandle.USER_NULL); + int changeEventId = intent.getIntExtra( + NOTIFICATION_INTENT_EXTRA_CHANGE_ID, 0); + notificationSwipedAway(notifiedUserId, changeEventId); + break; + default: + Log.d(TAG, "Unknown intent action received: " + intent.getAction()); + } + } + }; + + private final Object mConfigurationLock = new Object(); + @GuardedBy("mConfigurationLock") + private ConfigurationInternal mConfigurationInternal; + @GuardedBy("mConfigurationLock") + private boolean mIsRegistered; + + private int mAcceptedManualChanges; + private int mAcceptedTelephonyChanges; + private int mAcceptedLocationChanges; + private int mAcceptedUnknownChanges; + private int mRejectedTelephonyChanges; + private int mRejectedLocationChanges; + private int mRejectedUnknownChanges; + + /** Create and initialise a new {@code TimeZoneChangeTrackerImpl} */ + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") + public static NotifyingTimeZoneChangeListener create(Handler handler, Context context, + ServiceConfigAccessor serviceConfigAccessor) { + NotifyingTimeZoneChangeListener changeTracker = + new NotifyingTimeZoneChangeListener(handler, + context, + serviceConfigAccessor, + context.getSystemService(NotificationManager.class)); + + // Pretend there was an update to initialize configuration. + changeTracker.handleConfigurationUpdate(); + + return changeTracker; + } + + @VisibleForTesting + NotifyingTimeZoneChangeListener( + Handler handler, Context context, ServiceConfigAccessor serviceConfigAccessor, + NotificationManager notificationManager) { + mHandler = Objects.requireNonNull(handler); + mContext = Objects.requireNonNull(context); + mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor); + mServiceConfigAccessor.addConfigurationInternalChangeListener( + this::handleConfigurationUpdate); + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mNotificationManager = notificationManager; + } + + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") + private void handleConfigurationUpdate() { + synchronized (mConfigurationLock) { + ConfigurationInternal oldConfigurationInternal = mConfigurationInternal; + mConfigurationInternal = mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + + if (areNotificationsEnabled() && isNotificationTrackingSupported()) { + if (!mIsRegistered) { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_NOTIFICATION_DELETED); + mContext.registerReceiverForAllUsers(mNotificationReceiver, intentFilter, + /* broadcastPermission= */ null, mHandler, RECEIVER_NOT_EXPORTED); + mIsRegistered = true; + } + } else if (mIsRegistered) { + mContext.unregisterReceiver(mNotificationReceiver); + mIsRegistered = false; + } + + if (oldConfigurationInternal != null) { + boolean userChanged = + oldConfigurationInternal.getUserId() != mConfigurationInternal.getUserId(); + + if (!areNotificationsEnabled() || userChanged) { + // Clear any notifications that are no longer needed. + clearNotificationForUser(oldConfigurationInternal.getUserId()); + } + } + } + } + + private void notificationSwipedAway(@UserIdInt int userId, int changeEventId) { + // User swiping away a notification is interpreted as "user accepted the change". + if (isNotificationTrackingSupported()) { + markChangeAsAccepted(changeEventId, userId, SIGNAL_TYPE_NOTIFICATION); + } + } + + private boolean areNotificationsEnabled() { + synchronized (mConfigurationLock) { + return mConfigurationInternal.getNotificationsEnabledBehavior(); + } + } + + private boolean isNotificationTrackingSupported() { + synchronized (mConfigurationLock) { + return mConfigurationInternal.isNotificationTrackingSupported(); + } + } + + private boolean isManualChangeTrackingSupported() { + synchronized (mConfigurationLock) { + return mConfigurationInternal.isManualChangeTrackingSupported(); + } + } + + /** + * Marks a change event as accepted by the user + * + * <p>A change event is said to be accepted when the client does not revert an automatic time + * zone change by manually changing the time zone within {@code AUTO_REVERT_THRESHOLD} of the + * notification being received. + */ + private void markChangeAsAccepted(int changeEventId, @UserIdInt int userId, + @SignalType int signalType) { + if (!isUserIdCurrentUser(userId)) { + return; + } + + synchronized (mTimeZoneChangeRecord) { + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + if (lastTimeZoneChangeRecord != null) { + if (lastTimeZoneChangeRecord.getId() != changeEventId) { + // To be accepted, the change being accepted has to still be the latest. + return; + } + if (lastTimeZoneChangeRecord.getStatus() != STATUS_UNKNOWN) { + // Change status has already been set. + return; + } + lastTimeZoneChangeRecord.setAccepted(signalType); + + switch (lastTimeZoneChangeRecord.getEvent().getOrigin()) { + case ORIGIN_MANUAL: + mAcceptedManualChanges += 1; + break; + case ORIGIN_TELEPHONY: + mAcceptedTelephonyChanges += 1; + break; + case ORIGIN_LOCATION: + mAcceptedLocationChanges += 1; + break; + default: + mAcceptedUnknownChanges += 1; + break; + } + } + } + } + + private boolean isUserIdCurrentUser(@UserIdInt int userId) { + synchronized (mConfigurationLock) { + return userId == mConfigurationInternal.getUserId(); + } + } + + /** + * Marks a change event as rejected by the user + * + * <p>A change event is said to be rejected when the client reverts an automatic time zone + * change by manually changing the time zone within {@code AUTO_REVERT_THRESHOLD} of the + * notification being received. + */ + @GuardedBy("mTimeZoneChangeRecord") + private void markChangeAsRejected(int changeEventId, @UserIdInt int userId, + @SignalType int signalType) { + if (!isUserIdCurrentUser(userId)) { + return; + } + + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + if (lastTimeZoneChangeRecord != null) { + if (lastTimeZoneChangeRecord.getId() != changeEventId) { + // To be accepted, the change being accepted has to still be the latest. + return; + } + if (lastTimeZoneChangeRecord.getStatus() != STATUS_UNKNOWN) { + // Change status has already been set. + return; + } + lastTimeZoneChangeRecord.setRejected(signalType); + + switch (lastTimeZoneChangeRecord.getEvent().getOrigin()) { + case ORIGIN_TELEPHONY: + mRejectedTelephonyChanges += 1; + break; + case ORIGIN_LOCATION: + mRejectedLocationChanges += 1; + break; + default: + mRejectedUnknownChanges += 1; + break; + } + } + } + + @Override + public void process(TimeZoneChangeEvent changeEvent) { + final TimeZoneChangeRecord trackedChangeEvent; + + synchronized (mTimeZoneChangeRecord) { + fixPotentialHistoryCorruption(changeEvent); + + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + int changeEventId = mNextChangeEventId.getAndIncrement(); + trackedChangeEvent = new TimeZoneChangeRecord(changeEventId, changeEvent); + + if (isManualChangeTrackingSupported()) { + // Time-based heuristic for "user is undoing a mistake made by the time zone + // detector". + if (lastTimeZoneChangeRecord != null + && lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) { + TimeZoneChangeEvent lastChangeEvent = lastTimeZoneChangeRecord.getEvent(); + + if (shouldRejectChangeEvent(changeEvent, lastChangeEvent)) { + markChangeAsRejected(lastTimeZoneChangeRecord.getId(), + changeEvent.getUserId(), SIGNAL_TYPE_HEURISTIC); + } + } + + // Schedule a callback for the new time zone so that we can implement "user accepted + // the change because they didn't revert it" + scheduleChangeAcceptedHeuristicCallback(trackedChangeEvent, AUTO_REVERT_THRESHOLD); + } + + if (lastTimeZoneChangeRecord != null + && lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) { + lastTimeZoneChangeRecord.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE); + } + + if (changeEvent.getOrigin() == ORIGIN_MANUAL) { + trackedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + } + + mTimeZoneChangeRecord.set(trackedChangeEvent); + } + + if (areNotificationsEnabled()) { + int currentUserId; + synchronized (mConfigurationLock) { + currentUserId = mConfigurationInternal.getUserId(); + } + + if (changeEvent.getOrigin() == ORIGIN_MANUAL) { + // Just clear any existing notification. + clearNotificationForUser(currentUserId); + } else { + notifyOfTimeZoneChange(currentUserId, trackedChangeEvent); + } + } + } + + /** + * Checks if the history of time zone change events is corrupted and fixes it, if needed + * + * <p>The history of changes is considered corrupted if a transition is missing. That is, if + * {@code events[i-1].newTimeZoneId != events[i].oldTimeZoneId}. In that case, a "synthetic" + * event is added to the history to bridge the gap between the last reported time zone ID and + * the time zone ID that the new event is replacing. + * + * <p>Note: we are not expecting this method to be required often (if ever) but in the + * eventuality that an event gets lost, we want to keep the history coherent. + */ + @GuardedBy("mTimeZoneChangeRecord") + private void fixPotentialHistoryCorruption(TimeZoneChangeEvent changeEvent) { + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + + if (lastTimeZoneChangeRecord != null) { + // The below block takes care of the case where we are missing record(s) of time + // zone changes + TimeZoneChangeEvent lastChangeEvent = lastTimeZoneChangeRecord.getEvent(); + if (!changeEvent.getOldZoneId().equals(lastChangeEvent.getNewZoneId())) { + int changeEventId = mNextChangeEventId.getAndIncrement(); + TimeZoneChangeEvent syntheticChangeEvent = new TimeZoneChangeEvent( + SystemClock.elapsedRealtime(), System.currentTimeMillis(), + ORIGIN_UNKNOWN, UserHandle.USER_NULL, lastChangeEvent.getNewZoneId(), + changeEvent.getOldZoneId(), 0, "Synthetic"); + TimeZoneChangeRecord syntheticTrackedChangeEvent = + new TimeZoneChangeRecord(changeEventId, syntheticChangeEvent); + syntheticTrackedChangeEvent.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE); + + mTimeZoneChangeRecord.set(syntheticTrackedChangeEvent); + + // Housekeeping for the last reported time zone change: try to ensure it has + // a status too. + if (lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) { + lastTimeZoneChangeRecord.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE); + } + } + } + } + + private static boolean shouldRejectChangeEvent(TimeZoneChangeEvent changeEvent, + TimeZoneChangeEvent lastChangeEvent) { + return changeEvent.getOrigin() == ORIGIN_MANUAL + && lastChangeEvent.getOrigin() != ORIGIN_MANUAL + && (changeEvent.getElapsedRealtimeMillis() + - lastChangeEvent.getElapsedRealtimeMillis() < AUTO_REVERT_THRESHOLD); + } + + private void scheduleChangeAcceptedHeuristicCallback( + TimeZoneChangeRecord trackedChangeEvent, + @DurationMillisLong long delayMillis) { + mHandler.postDelayed( + () -> changeAcceptedTimeHeuristicCallback(trackedChangeEvent.getId()), delayMillis); + } + + private void changeAcceptedTimeHeuristicCallback(int changeEventId) { + if (isManualChangeTrackingSupported()) { + int currentUserId = mActivityManagerInternal.getCurrentUserId(); + markChangeAsAccepted(changeEventId, currentUserId, SIGNAL_TYPE_HEURISTIC); + } + } + + private void clearNotificationForUser(@UserIdInt int userId) { + mNotificationManager.cancelAsUser(NOTIFICATION_TAG, TZ_CHANGE_NOTIFICATION_ID, + UserHandle.of(userId)); + } + + private void notifyOfTimeZoneChange(@UserIdInt int userId, + TimeZoneChangeRecord trackedChangeEvent) { + TimeZoneChangeEvent changeEvent = trackedChangeEvent.getEvent(); + + if (!Flags.datetimeNotifications() || !areNotificationsEnabled()) { + return; + } + + TimeZone oldTimeZone = TimeZone.getTimeZone(changeEvent.getOldZoneId()); + TimeZone newTimeZone = TimeZone.getTimeZone(changeEvent.getNewZoneId()); + long unixEpochTimeMillis = changeEvent.getUnixEpochTimeMillis(); + boolean hasOffsetChanged = newTimeZone.getOffset(unixEpochTimeMillis) + == oldTimeZone.getOffset(unixEpochTimeMillis); + + if (hasOffsetChanged) { + // If the time zone ID changes but not the offset, we do not send a notification to + // the user. This is to prevent spamming users and reduce the number of notification + // we send overall. + Log.d(TAG, "The time zone ID has changed but the offset remains the same."); + return; + } + + final CharSequence title = mRes.getString(R.string.time_zone_change_notification_title); + final CharSequence body = getNotificationBody(newTimeZone, unixEpochTimeMillis); + + final Intent clickNotificationIntent = new Intent(ACTION_DATE_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + final Intent clearNotificationIntent = new Intent(ACTION_NOTIFICATION_DELETED) + .putExtra(NOTIFICATION_INTENT_EXTRA_USER_ID, userId) + .putExtra(NOTIFICATION_INTENT_EXTRA_CHANGE_ID, trackedChangeEvent.getId()); + + Notification notification = new Notification.Builder(mContext, + SystemNotificationChannels.TIME) + .setSmallIcon(R.drawable.btn_clock_material) + .setStyle(new Notification.BigTextStyle().bigText(body)) + .setOnlyAlertOnce(true) + .setColor(mContext.getColor(R.color.system_notification_accent_color)) + .setTicker(title) + .setContentTitle(title) + .setContentText(body) + .setContentIntent(PendingIntent.getActivityAsUser( + mContext, + /* requestCode= */ 0, + clickNotificationIntent, + /* flags= */ FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, + /* options= */ null, + UserHandle.of(userId))) + .setDeleteIntent(PendingIntent.getBroadcast( + mContext, + /* requestCode= */ 0, + clearNotificationIntent, + /* flags= */ FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)) + .setAutoCancel(true) // auto-clear notification on selection + .build(); + + mNotificationManager.notifyAsUser(NOTIFICATION_TAG, + TZ_CHANGE_NOTIFICATION_ID, notification, UserHandle.of(userId)); + } + + private CharSequence getNotificationBody(TimeZone newTimeZone, long unixEpochTimeMillis) { + DateFormat timeFormat = SimpleDateFormat.getInstanceForSkeleton("zzzz"); + DateFormat offsetFormat = SimpleDateFormat.getInstanceForSkeleton("ZZZZ"); + + String newTime = formatInZone(timeFormat, newTimeZone, unixEpochTimeMillis); + String newOffset = formatInZone(offsetFormat, newTimeZone, unixEpochTimeMillis); + + return mRes.getString(R.string.time_zone_change_notification_body, newTime, newOffset); + } + + private static String formatInZone(DateFormat timeFormat, TimeZone timeZone, + long unixEpochTimeMillis) { + timeFormat.setTimeZone(timeZone); + return timeFormat.format(unixEpochTimeMillis); + } + + @Override + public void dump(IndentingPrintWriter pw) { + synchronized (mConfigurationLock) { + pw.println("currentUserId=" + mConfigurationInternal.getUserId()); + pw.println("notificationsEnabledBehavior=" + + mConfigurationInternal.getNotificationsEnabledBehavior()); + pw.println("notificationTrackingSupported=" + + mConfigurationInternal.isNotificationTrackingSupported()); + pw.println("manualChangeTrackingSupported=" + + mConfigurationInternal.isManualChangeTrackingSupported()); + } + + pw.println("mAcceptedLocationChanges=" + mAcceptedLocationChanges); + pw.println("mAcceptedManualChanges=" + mAcceptedManualChanges); + pw.println("mAcceptedTelephonyChanges=" + mAcceptedTelephonyChanges); + pw.println("mAcceptedUnknownChanges=" + mAcceptedUnknownChanges); + pw.println("mRejectedLocationChanges=" + mRejectedLocationChanges); + pw.println("mRejectedTelephonyChanges=" + mRejectedTelephonyChanges); + pw.println("mRejectedUnknownChanges=" + mRejectedUnknownChanges); + pw.println("mNextChangeEventId=" + mNextChangeEventId); + + pw.println("mTimeZoneChangeRecord:"); + pw.increaseIndent(); + synchronized (mTimeZoneChangeRecord) { + mTimeZoneChangeRecord.dump(pw); + } + pw.decreaseIndent(); + } + + @VisibleForTesting + static class TimeZoneChangeRecord { + + private final int mId; + private final TimeZoneChangeEvent mEvent; + private @TimeZoneChangeStatus int mStatus = STATUS_UNKNOWN; + private @SignalType int mSignalType = SIGNAL_TYPE_UNKNOWN; + + TimeZoneChangeRecord(int id, TimeZoneChangeEvent event) { + mId = id; + mEvent = Objects.requireNonNull(event); + } + + public int getId() { + return mId; + } + + public @TimeZoneChangeStatus int getStatus() { + return mStatus; + } + + public void setAccepted(int signalType) { + setStatus(STATUS_ACCEPTED, signalType); + } + + public void setRejected(int signalType) { + setStatus(STATUS_REJECTED, signalType); + } + + public void setStatus(@TimeZoneChangeStatus int status, @SignalType int signalType) { + mStatus = status; + mSignalType = signalType; + } + + public TimeZoneChangeEvent getEvent() { + return mEvent; + } + + @Override + public String toString() { + return "TrackedTimeZoneChangeEvent{" + + "mId=" + mId + + ", mEvent=" + mEvent + + ", mStatus=" + mStatus + + ", mSignalType=" + mSignalType + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TimeZoneChangeRecord that) { + return mId == that.mId + && mEvent.equals(that.mEvent) + && mStatus == that.mStatus + && mSignalType == that.mSignalType; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(mId, mEvent, mStatus, mSignalType); + } + } + + @VisibleForTesting + TimeZoneChangeRecord getLastTimeZoneChangeRecord() { + synchronized (mTimeZoneChangeRecord) { + return mTimeZoneChangeRecord.get(); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java index e14326cc2d53..d340ed470591 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java @@ -102,5 +102,29 @@ public interface TimeZoneChangeListener { + ", mCause='" + mCause + '\'' + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TimeZoneChangeEvent that) { + return mElapsedRealtimeMillis == that.mElapsedRealtimeMillis + && mUnixEpochTimeMillis == that.mUnixEpochTimeMillis + && mOrigin == that.mOrigin + && mUserId == that.mUserId + && Objects.equals(mOldZoneId, that.mOldZoneId) + && Objects.equals(mNewZoneId, that.mNewZoneId) + && mNewConfidence == that.mNewConfidence + && Objects.equals(mCause, that.mCause); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(mElapsedRealtimeMillis, mUnixEpochTimeMillis, mOrigin, mUserId, + mOldZoneId, mNewZoneId, mNewConfidence, mCause); + } } } diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java index 19a28ddcdaeb..b2b06b0af5fa 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java @@ -42,6 +42,7 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.content.Context; import android.os.Handler; +import android.os.SystemClock; import android.os.TimestampedValue; import android.os.UserHandle; import android.util.IndentingPrintWriter; @@ -50,6 +51,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.SystemTimeZone.TimeZoneConfidence; +import com.android.server.flags.Flags; import com.android.server.timezonedetector.ConfigurationInternal.DetectionMode; import java.io.PrintWriter; @@ -215,6 +217,13 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>(); /** + * A component adjunct to the detection behavior that tracks time zone changes and implements + * behavior associated with time zone changes. + */ + @NonNull + private final TimeZoneChangeListener mChangeTracker; + + /** * A snapshot of the current detector status. A local copy is cached because it is relatively * heavyweight to obtain and is used more often than it is expected to change. */ @@ -256,15 +265,20 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat @NonNull ServiceConfigAccessor serviceConfigAccessor) { Environment environment = new EnvironmentImpl(handler); - return new TimeZoneDetectorStrategyImpl(serviceConfigAccessor, environment); + TimeZoneChangeListener changeEventTracker = + NotifyingTimeZoneChangeListener.create(handler, context, serviceConfigAccessor); + return new TimeZoneDetectorStrategyImpl( + serviceConfigAccessor, environment, changeEventTracker); } @VisibleForTesting public TimeZoneDetectorStrategyImpl( @NonNull ServiceConfigAccessor serviceConfigAccessor, - @NonNull Environment environment) { + @NonNull Environment environment, + @NonNull TimeZoneChangeListener changeEventTracker) { mEnvironment = Objects.requireNonNull(environment); mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor); + mChangeTracker = Objects.requireNonNull(changeEventTracker); // Start with telephony fallback enabled. mTelephonyTimeZoneFallbackEnabled = @@ -833,6 +847,17 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat Slog.d(LOG_TAG, logInfo); } mEnvironment.setDeviceTimeZoneAndConfidence(newZoneId, newConfidence, logInfo); + + if (Flags.datetimeNotifications()) { + // Record the fact that the time zone was changed so that it can be tracked, i.e. + // whether the device / user sticks with it. + TimeZoneChangeListener.TimeZoneChangeEvent changeEvent = + new TimeZoneChangeListener.TimeZoneChangeEvent( + SystemClock.elapsedRealtime(), System.currentTimeMillis(), origin, + userId, + currentZoneId, newZoneId, newConfidence, cause); + mChangeTracker.process(changeEvent); + } } @GuardedBy("this") @@ -947,6 +972,14 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat ipw.increaseIndent(); // level 2 mTelephonySuggestionsBySlotIndex.dump(ipw); ipw.decreaseIndent(); // level 2 + + if (Flags.datetimeNotifications()) { + ipw.println("Time zone change tracker:"); + ipw.increaseIndent(); // level 2 + mChangeTracker.dump(ipw); + ipw.decreaseIndent(); // level 2 + } + ipw.decreaseIndent(); // level 1 } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index ef6f92317b2c..12c8f9ccac7c 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2538,7 +2538,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { void wakeUp(int displayId, String reason) { mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_APPLICATION, - "android.server.am:TURN_ON:" + reason, displayId); + "android.server.wm:TURN_ON:" + reason, displayId); } /** Starts a batch of visibility updates. */ diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index acd47dad83a1..d32c31f1c1c7 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4295,7 +4295,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return target; } if (android.view.inputmethod.Flags.refactorInsetsController()) { - final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked(); + final DisplayContent defaultDc = getUserMainDisplayContent(); return defaultDc.mRemoteInsetsControlTarget; } else { return getImeFallback(); @@ -4305,11 +4305,26 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp InsetsControlTarget getImeFallback() { // host is in non-default display that doesn't support system decor, default to // default display's StatusBar to control IME (when available), else let system control it. - final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked(); - WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar(); + final DisplayContent defaultDc = getUserMainDisplayContent(); + final WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar(); return statusBar != null ? statusBar : defaultDc.mRemoteInsetsControlTarget; } + private DisplayContent getUserMainDisplayContent() { + final DisplayContent defaultDc; + if (android.view.inputmethod.Flags.fallbackDisplayForSecondaryUserOnSecondaryDisplay()) { + final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId); + defaultDc = mWmService.getUserMainDisplayContentLocked(userId); + if (defaultDc == null) { + throw new IllegalStateException( + "No default display was assigned to user " + userId); + } + } else { + defaultDc = mWmService.getDefaultDisplayContentLocked(); + } + return defaultDc; + } + /** * Returns the corresponding IME insets control target according the IME target type. * @@ -4845,8 +4860,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // The control target could be the RemoteInsetsControlTarget (if the focussed // view is on a virtual display that can not show the IME (and therefore it will // be shown on the default display) - if (isDefaultDisplay && mRemoteInsetsControlTarget != null) { - return mRemoteInsetsControlTarget; + if (android.view.inputmethod.Flags + .fallbackDisplayForSecondaryUserOnSecondaryDisplay()) { + if (isUserMainDisplay() && mRemoteInsetsControlTarget != null) { + return mRemoteInsetsControlTarget; + } + } else { + if (isDefaultDisplay && mRemoteInsetsControlTarget != null) { + return mRemoteInsetsControlTarget; + } } } return null; @@ -4862,6 +4884,16 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * Returns {@code true} if {@link #mDisplayId} corresponds to the user's main display. + * + * <p>Visible background users may have other than DEFAULT_DISPLAY marked as their main display. + */ + private boolean isUserMainDisplay() { + final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId); + return mDisplayId == mWmService.mUmInternal.getMainDisplayAssignedToUser(userId); + } + + /** * Computes the window the IME should be attached to. */ @VisibleForTesting diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java index 3c60d8296577..db058cafe5fe 100644 --- a/services/core/java/com/android/server/wm/DragDropController.java +++ b/services/core/java/com/android/server/wm/DragDropController.java @@ -215,7 +215,8 @@ class DragDropController { mDragState.mOriginalAlpha = alpha; mDragState.mAnimatedScale = callingWin.mGlobalScale; mDragState.mToken = dragToken; - mDragState.mDisplayContent = displayContent; + mDragState.mStartDragDisplayContent = displayContent; + mDragState.mCurrentDisplayContent = displayContent; mDragState.mData = data; mDragState.mCallingTaskIdToHide = shouldMoveCallingTaskToBack(callingWin, flags); @@ -273,7 +274,7 @@ class DragDropController { InputManagerGlobal.getInstance().setPointerIcon( PointerIcon.getSystemIcon( mService.mContext, PointerIcon.TYPE_GRABBING), - mDragState.mDisplayContent.getDisplayId(), touchDeviceId, + mDragState.mCurrentDisplayContent.getDisplayId(), touchDeviceId, touchPointerId, mDragState.getInputToken()); } // remember the thumb offsets for later diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index b3e9244d108d..d48b9b4a5d10 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -129,10 +129,17 @@ class DragState { */ volatile boolean mAnimationCompleted = false; /** + * The display on which the drag originally started. Note that it's possible for either/both + * mStartDragDisplayContent and mCurrentDisplayContent to be invalid if DisplayTopology was + * changed or removed in the middle of the drag. In this case, drag will also be cancelled as + * soon as listener is notified. + */ + DisplayContent mStartDragDisplayContent; + /** * The display on which the drag is happening. If it goes into a different display this will * be updated. */ - DisplayContent mDisplayContent; + DisplayContent mCurrentDisplayContent; @Nullable private ValueAnimator mAnimator; private final Interpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); @@ -181,7 +188,7 @@ class DragState { .setContainerLayer() .setName("Drag and Drop Input Consumer") .setCallsite("DragState.showInputSurface") - .setParent(mDisplayContent.getOverlayLayer()) + .setParent(mCurrentDisplayContent.getOverlayLayer()) .build(); } final InputWindowHandle h = getInputWindowHandle(); @@ -246,7 +253,8 @@ class DragState { } } DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, inWindowX, - inWindowY, mThumbOffsetX, mThumbOffsetY, mFlags, null, null, null, + inWindowY, mThumbOffsetX, mThumbOffsetY, + mCurrentDisplayContent.getDisplayId(), mFlags, null, null, null, dragSurface, null, mDragResult); try { if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DRAG_ENDED to " + ws); @@ -549,7 +557,7 @@ class DragState { PointF relativeToWindowCoords = new PointF(newWin.translateToWindowX(touchX), newWin.translateToWindowY(touchY)); if (Flags.enableConnectedDisplaysDnd() - && mDisplayContent.getDisplayId() != newWin.getDisplayId()) { + && mCurrentDisplayContent.getDisplayId() != newWin.getDisplayId()) { // Currently DRAG_STARTED coords are sent relative to the window target in **px** // coordinates. However, this cannot be extended to connected displays scenario, // as there's only global **dp** coordinates and no global **px** coordinates. @@ -720,6 +728,20 @@ class DragState { mCurrentDisplayX = displayX; mCurrentDisplayY = displayY; + final DisplayContent lastSetDisplayContent = mCurrentDisplayContent; + boolean cursorMovedToDifferentDisplay = false; + // Keep latest display up-to-date even when drag has stopped. + if (Flags.enableConnectedDisplaysDnd() && mCurrentDisplayContent.mDisplayId != displayId) { + final DisplayContent newDisplay = mService.mRoot.getDisplayContent(displayId); + if (newDisplay == null) { + Slog.e(TAG_WM, "Target displayId=" + displayId + " was not found, ending drag."); + endDragLocked(false /* dropConsumed */, + false /* relinquishDragSurfaceToDropTarget */); + return; + } + cursorMovedToDifferentDisplay = true; + mCurrentDisplayContent = newDisplay; + } if (!keepHandling) { return; } @@ -728,6 +750,24 @@ class DragState { if (SHOW_LIGHT_TRANSACTIONS) { Slog.i(TAG_WM, ">>> OPEN TRANSACTION notifyMoveLocked"); } + if (cursorMovedToDifferentDisplay) { + mAnimatedScale = mAnimatedScale * mCurrentDisplayContent.mBaseDisplayDensity + / lastSetDisplayContent.mBaseDisplayDensity; + mThumbOffsetX = mThumbOffsetX * mCurrentDisplayContent.mBaseDisplayDensity + / lastSetDisplayContent.mBaseDisplayDensity; + mThumbOffsetY = mThumbOffsetY * mCurrentDisplayContent.mBaseDisplayDensity + / lastSetDisplayContent.mBaseDisplayDensity; + mTransaction.reparent(mSurfaceControl, mCurrentDisplayContent.getSurfaceControl()); + mTransaction.setScale(mSurfaceControl, mAnimatedScale, mAnimatedScale); + + final InputWindowHandle inputWindowHandle = getInputWindowHandle(); + if (inputWindowHandle == null) { + Slog.w(TAG_WM, "Drag is in progress but there is no drag window handle."); + return; + } + inputWindowHandle.displayId = displayId; + mTransaction.setInputWindowInfo(mInputSurface, inputWindowHandle); + } mTransaction.setPosition(mSurfaceControl, displayX - mThumbOffsetX, displayY - mThumbOffsetY).apply(); ProtoLog.i(WM_SHOW_TRANSACTIONS, "DRAG %s: displayId=%d, pos=(%d,%d)", mSurfaceControl, @@ -752,10 +792,10 @@ class DragState { ClipData data, boolean includeDragSurface, boolean includeDragFlags, IDragAndDropPermissions dragAndDropPermissions) { return DragEvent.obtain(action, x, y, mThumbOffsetX, mThumbOffsetY, - includeDragFlags ? mFlags : 0, + mCurrentDisplayContent.getDisplayId(), includeDragFlags ? mFlags : 0, null /* localState */, description, data, - includeDragSurface ? mSurfaceControl : null, - dragAndDropPermissions, false /* result */); + includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions, + false /* result */); } private ValueAnimator createReturnAnimationLocked() { diff --git a/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java b/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java index 8c50913dd563..29922f0f85c5 100644 --- a/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java +++ b/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java @@ -57,9 +57,11 @@ class PageSizeMismatchDialog extends AppWarnings.BaseDialog { final AlertDialog.Builder builder = new AlertDialog.Builder(context) - .setPositiveButton( - R.string.ok, - (dialog, which) -> {/* Do nothing */}) + .setPositiveButton(R.string.ok, (dialog, which) -> + manager.setPackageFlag( + mUserId, mPackageName, + AppWarnings.FLAG_HIDE_PAGE_SIZE_MISMATCH, + true)) .setMessage(Html.fromHtml(warning, FROM_HTML_MODE_COMPACT)) .setTitle(label); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 9c1cf6e6bf62..fe478c60bc32 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5230,9 +5230,15 @@ class Task extends TaskFragment { // to ensure any necessary pause logic occurs. In the case where the Activity will be // shown regardless of the lock screen, the call to // {@link ActivityTaskSupervisor#checkReadyForSleepLocked} is skipped. - final ActivityRecord next = topRunningActivity(true /* focusableOnly */); - if (next == null || !next.canTurnScreenOn()) { - checkReadyForSleep(); + if (shouldSleepActivities()) { + final ActivityRecord next = topRunningActivity(true /* focusableOnly */); + if (next != null && next.canTurnScreenOn() + && !mWmService.mPowerManager.isInteractive()) { + mTaskSupervisor.wakeUp(getDisplayId(), "resumeTop-turnScreenOnFlag"); + next.setCurrentLaunchCanTurnScreenOn(false); + } else { + checkReadyForSleep(); + } } } finally { mInResumeTopActivity = false; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 54a3d4179e3d..e761e024b3ca 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -117,6 +117,7 @@ import com.android.server.wm.SurfaceAnimator.Animatable; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; import com.android.server.wm.utils.AlwaysTruePredicate; +import com.android.window.flags.Flags; import java.io.PrintWriter; import java.lang.ref.WeakReference; @@ -2736,6 +2737,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< if (!mTransitionController.canAssignLayers(this)) return; final boolean changed = layer != mLastLayer || mLastRelativeToLayer != null; if (mSurfaceControl != null && changed) { + if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + // When this container needs to be synced, assign layer with its own sync + // transaction to avoid out of ordering when merge. + // Still use the passed-in transaction for non-sync case, such as building finish + // transaction. + t = getSyncTransaction(); + } setLayer(t, layer); mLastLayer = layer; mLastRelativeToLayer = null; @@ -2746,6 +2754,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< boolean forceUpdate) { final boolean changed = layer != mLastLayer || mLastRelativeToLayer != relativeTo; if (mSurfaceControl != null && (changed || forceUpdate)) { + if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + // When this container needs to be synced, assign layer with its own sync + // transaction to avoid out of ordering when merge. + // Still use the passed-in transaction for non-sync case, such as building finish + // transaction. + t = getSyncTransaction(); + } setRelativeLayer(t, relativeTo, layer); mLastLayer = layer; mLastRelativeToLayer = relativeTo; diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 92e0931993d1..e4ef3d186bdb 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -7473,6 +7473,23 @@ public class WindowManagerService extends IWindowManager.Stub return mRoot.getDisplayContent(DEFAULT_DISPLAY); } + /** + * Returns the main display content for the user passed as parameter. + * + * <p>Visible background users may have their own designated main display, distinct from the + * system default display (DEFAULT_DISPLAY). Visible background users operate independently + * with their own main displays. These secondary user main displays host the secondary home + * activities. + */ + @Nullable + DisplayContent getUserMainDisplayContentLocked(@UserIdInt int userId) { + final int userMainDisplayId = mUmInternal.getMainDisplayAssignedToUser(userId); + if (userMainDisplayId == -1) { + return null; + } + return mRoot.getDisplayContent(userMainDisplayId); + } + public void onOverlayChanged() { // Post to display thread so it can get the latest display info. mH.post(() -> { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index d69b06ad71ea..85e3d89730a3 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5562,6 +5562,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @Override void assignLayer(Transaction t, int layer) { if (mStartingData != null) { + if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + // When this container needs to be synced, assign layer with its own sync + // transaction to avoid out of ordering when merge. + // Still use the passed-in transaction for non-sync case, such as building finish + // transaction. + t = getSyncTransaction(); + } // The starting window should cover the task. t.setLayer(mSurfaceControl, Integer.MAX_VALUE); return; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index d2d388401e23..0ce25db6ea55 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -10470,6 +10470,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // Reset some of the user-specific policies. final DevicePolicyData policy = getUserData(userId); policy.mPermissionPolicy = DevicePolicyManager.PERMISSION_POLICY_PROMPT; + mPolicyCache.setPermissionPolicy(userId, policy.mPermissionPolicy); // Clear delegations. policy.mDelegationMap.clear(); policy.mStatusBarDisabled = false; diff --git a/services/print/java/com/android/server/print/flags.aconfig b/services/print/java/com/android/server/print/flags.aconfig index 0210791cfeda..42d142545660 100644 --- a/services/print/java/com/android/server/print/flags.aconfig +++ b/services/print/java/com/android/server/print/flags.aconfig @@ -3,7 +3,7 @@ container: "system" flag { name: "do_not_include_capabilities" - namespace: "print" + namespace: "printing" description: "Do not use the flag Context.BIND_INCLUDE_CAPABILITIES when binding to the service" bug: "291281543" } diff --git a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java index 4e9fff230bac..b80d68de0f2e 100644 --- a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java +++ b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java @@ -37,6 +37,7 @@ import static org.testng.Assert.expectThrows; import android.annotation.UserIdInt; import android.app.Application; +import android.app.backup.BackupManagerInternal; import android.app.backup.IBackupManagerMonitor; import android.app.backup.IBackupObserver; import android.app.backup.IFullBackupRestoreObserver; @@ -52,6 +53,7 @@ import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.util.SparseArray; +import com.android.server.LocalServices; import com.android.server.SystemService.TargetUser; import com.android.server.backup.testing.TransportData; import com.android.server.testing.shadows.ShadowApplicationPackageManager; @@ -229,7 +231,7 @@ public class BackupManagerServiceRoboTest { setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false); IBinder agentBinder = mock(IBinder.class); - backupManagerService.agentConnected(mUserOneId, TEST_PACKAGE, agentBinder); + backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserOneId, agentBinder); verify(mUserOneBackupAgentConnectionManager).agentConnected(TEST_PACKAGE, agentBinder); } @@ -242,7 +244,7 @@ public class BackupManagerServiceRoboTest { setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false); IBinder agentBinder = mock(IBinder.class); - backupManagerService.agentConnected(mUserTwoId, TEST_PACKAGE, agentBinder); + backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserTwoId, agentBinder); verify(mUserOneBackupAgentConnectionManager, never()).agentConnected(TEST_PACKAGE, agentBinder); @@ -1549,6 +1551,7 @@ public class BackupManagerServiceRoboTest { @Test public void testOnStart_publishesService() { BackupManagerService backupManagerService = mock(BackupManagerService.class); + LocalServices.removeServiceForTest(BackupManagerInternal.class); BackupManagerService.Lifecycle lifecycle = spy(new BackupManagerService.Lifecycle(mContext, backupManagerService)); doNothing().when(lifecycle).publishService(anyString(), any()); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java index 6defadf44d05..35ab2d233563 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -1454,6 +1454,18 @@ public class ActivityManagerServiceTest { assertThat(tokenForFullIntent.getKeyFields()).isEqualTo(tokenForCloneIntent.getKeyFields()); } + @Test + public void testCanLaunchClipDataIntent() { + ClipData clipData = ClipData.newIntent("test", new Intent("test")); + clipData.prepareToLeaveProcess(true); + // skip mimicking sending clipData to another app because it will just be parceled and + // un-parceled. + Intent intent = clipData.getItemAt(0).getIntent(); + // default intent redirect protection won't block an intent nested in a top level ClipData. + assertThat(intent.getExtendedFlags() + & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0); + } + private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq, long lastNetworkUpdatedProcStateSeq, final long procStateSeqToWait, boolean expectWait) throws Exception { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 4a09802fc822..fe7cc923d3d1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -743,6 +743,43 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_receivers() { + final ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true); + + updateOomAdj(app); + assertNoCpuTime(app); + + app.mReceivers.incrementCurReceivers(); + updateOomAdj(app); + assertCpuTime(app); + + app.mReceivers.decrementCurReceivers(); + updateOomAdj(app); + assertNoCpuTime(app); + } + + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_activeInstrumentation() { + ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, + MOCKAPP_PACKAGENAME, true); + updateOomAdj(app); + assertNoCpuTime(app); + + mProcessStateController.setActiveInstrumentation(app, mock(ActiveInstrumentation.class)); + updateOomAdj(app); + assertCpuTime(app); + + mProcessStateController.setActiveInstrumentation(app, null); + updateOomAdj(app); + assertNoCpuTime(app); + } + + @SuppressWarnings("GuardedBy") + @Test public void testUpdateOomAdj_DoOne_OverlayUi() { ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java index c4a042370de5..f1f4a0e83a79 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.when; import android.Manifest; import android.app.backup.BackupManager; +import android.app.backup.BackupManagerInternal; import android.app.backup.ISelectBackupTransportCallback; import android.app.job.JobScheduler; import android.content.ComponentName; @@ -59,6 +60,7 @@ import androidx.test.filters.SmallTest; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.internal.util.DumpUtils; +import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.backup.utils.RandomAccessFileUtils; @@ -716,7 +718,7 @@ public class BackupManagerServiceTest { // Create BMS *before* setting a main user to simulate the main user being created after // BMS, which can happen for the first ever boot of a new device. mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER)); assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER)); @@ -730,7 +732,7 @@ public class BackupManagerServiceTest { // Create BMS *before* setting a main user to simulate the main user being created after // BMS, which can happen for the first ever boot of a new device. mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER)); assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER)); @@ -754,7 +756,7 @@ public class BackupManagerServiceTest { private void createBackupManagerServiceAndUnlockSystemUser() { mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); simulateUserUnlocked(UserHandle.USER_SYSTEM); } @@ -765,7 +767,15 @@ public class BackupManagerServiceTest { private void setMockMainUserAndCreateBackupManagerService(int userId) { when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(userId)); mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); + } + + private void createBackupServiceLifecycle(Context context, BackupManagerService service) { + // Anytime we manually create the Lifecycle, we need to remove the internal BMS because + // it would've been added already at boot time and LocalServices does not allow + // overriding an existing service. + LocalServices.removeServiceForTest(BackupManagerInternal.class); + mServiceLifecycle = new BackupManagerService.Lifecycle(context, service); } private void simulateUserUnlocked(int userId) { diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java index f1072da4161f..6d91bee6d3f6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java @@ -573,4 +573,14 @@ public class ThermalManagerServiceMockingTest { assertNotNull(ret); assertEquals(0, ret.size()); } + + @Test + public void forecastSkinTemperature() throws RemoteException { + Mockito.when(mAidlHalMock.forecastSkinTemperature(Mockito.anyInt())).thenReturn( + 0.55f + ); + float forecast = mAidlWrapper.forecastSkinTemperature(10); + Mockito.verify(mAidlHalMock, Mockito.times(1)).forecastSkinTemperature(10); + assertEquals(0.55f, forecast, 0.01f); + } } diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java index 3cb27451bd57..d9256247b835 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -89,6 +89,7 @@ import android.os.PowerManagerInternal; import android.os.PowerSaveState; import android.os.UserHandle; import android.os.test.TestLooper; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; @@ -119,6 +120,7 @@ import com.android.server.power.PowerManagerService.WakeLock; import com.android.server.power.batterysaver.BatterySaverController; import com.android.server.power.batterysaver.BatterySaverPolicy; import com.android.server.power.batterysaver.BatterySaverStateMachine; +import com.android.server.power.feature.flags.Flags; import com.android.server.power.feature.PowerManagerFlags; import com.android.server.testutils.OffsettableClock; @@ -3456,6 +3458,25 @@ public class PowerManagerServiceTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) + public void testAcquireWakelock_screenTimeoutListenersDisabled_noCrashes() { + IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); + when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); + + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP; + when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)) + .thenReturn(displayInfo); + + createService(); + startSystem(); + + acquireWakeLock("screenBright", PowerManager.SCREEN_BRIGHT_WAKE_LOCK, + Display.DEFAULT_DISPLAY); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) public void testAddWakeLockKeepingScreenOn_addsToNotifierAndReportsTimeoutPolicyChange() { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); @@ -3483,6 +3504,7 @@ public class PowerManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) public void test_addAndRemoveScreenTimeoutListener_propagatesToNotifier() throws Exception { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); @@ -3509,6 +3531,7 @@ public class PowerManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) public void test_displayIsRemoved_clearsScreenTimeoutListeners() throws Exception { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); @@ -3531,7 +3554,8 @@ public class PowerManagerServiceTest { } @Test - public void testScreenWakeLockListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier() + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) + public void testScreenTimeoutListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier() throws Exception { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java index e86108d84538..ede61a5a0269 100644 --- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java +++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java @@ -15,18 +15,14 @@ */ package com.android.server.selinux; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.android.server.selinux.SelinuxAuditLogBuilder.toCategories; import static com.google.common.truth.Truth.assertThat; -import android.provider.DeviceConfig; - import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,24 +41,12 @@ public class SelinuxAuditLogsBuilderTest { @Before public void setUp() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - TEST_DOMAIN)); - - mAuditLogBuilder = new SelinuxAuditLogBuilder(); + mAuditLogBuilder = new SelinuxAuditLogBuilder(TEST_DOMAIN); mScontextMatcher = mAuditLogBuilder.mScontextMatcher; mTcontextMatcher = mAuditLogBuilder.mTcontextMatcher; mPathMatcher = mAuditLogBuilder.mPathMatcher; } - @After - public void tearDown() { - runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides()); - } - @Test public void testMatcher_scontext() { assertThat(mScontextMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isTrue(); @@ -109,13 +93,9 @@ public class SelinuxAuditLogsBuilderTest { @Test public void testMatcher_scontextDefaultConfig() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.clearLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN)); - - Matcher scontexMatcher = new SelinuxAuditLogBuilder().mScontextMatcher; + Matcher scontexMatcher = + new SelinuxAuditLogBuilder(SelinuxAuditLogsCollector.DEFAULT_SELINUX_AUDIT_DOMAIN) + .mScontextMatcher; assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isFalse(); assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0:c123,c456").matches()) @@ -221,13 +201,7 @@ public class SelinuxAuditLogsBuilderTest { @Test public void testSelinuxAuditLogsBuilder_wrongConfig() { String notARegexDomain = "not]a[regex"; - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - notARegexDomain)); - SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder(); + SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder(notARegexDomain); noOpBuilder.reset( "granted { p } scontext=u:r:" diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java index b6ccf5e0ad80..db58c74e8431 100644 --- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java +++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java @@ -15,7 +15,6 @@ */ package com.android.server.selinux; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -28,7 +27,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; -import android.provider.DeviceConfig; import android.util.EventLog; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -59,6 +57,7 @@ public class SelinuxAuditLogsCollectorTest { private final SelinuxAuditLogsCollector mSelinuxAutidLogsCollector = // Ignore rate limiting for tests new SelinuxAuditLogsCollector( + () -> TEST_DOMAIN, new RateLimiter(mClock, /* window= */ Duration.ofMillis(0)), new QuotaLimiter( mClock, /* windowSize= */ Duration.ofHours(1), /* maxPermits= */ 5)); @@ -67,13 +66,6 @@ public class SelinuxAuditLogsCollectorTest { @Before public void setUp() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - TEST_DOMAIN)); - mSelinuxAutidLogsCollector.setStopRequested(false); // move the clock forward for the limiters. mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); @@ -85,7 +77,6 @@ public class SelinuxAuditLogsCollectorTest { @After public void tearDown() { - runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides()); mMockitoSession.finishMocking(); } diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java index 0bf419ec242c..998c1d1a23b1 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -35,6 +36,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.content.pm.UserInfo; import android.os.Binder; import android.os.Bundle; import android.os.DropBoxManager; @@ -48,6 +50,7 @@ import android.os.Parcel; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; +import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; import android.provider.Settings; @@ -68,6 +71,7 @@ import org.junit.Test; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -150,6 +154,25 @@ public class ActivityManagerTest { } @Test + public void testRemovedUserShouldNotBeRunning() throws Exception { + final UserManager userManager = mContext.getSystemService(UserManager.class); + assertNotNull("UserManager should not be null", userManager); + final UserInfo user = userManager.createUser( + "TestUser", UserManager.USER_TYPE_FULL_SECONDARY, 0); + + mService.startUserInBackground(user.id); + assertTrue("User should be running", mService.isUserRunning(user.id, 0)); + assertTrue("User should be in running users", + Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id)); + + userManager.removeUser(user.id); + mService.startUserInBackground(user.id); + assertFalse("Removed user should not be running", mService.isUserRunning(user.id, 0)); + assertFalse("Removed user should not be in running users", + Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id)); + } + + @Test public void testServiceUnbindAndKilling() { for (int i = TEST_LOOPS; i > 0; i--) { runOnce(i); diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 6411463fe0d9..06958b81d846 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -490,29 +490,6 @@ public class UserControllerTest { mInjector.mHandler.clearAllRecordedMessages(); // Verify that continueUserSwitch worked as expected continueAndCompleteUserSwitch(userState, oldUserId, newUserId); - verify(mInjector, times(0)).dismissKeyguard(any()); - verify(mInjector, times(1)).dismissUserSwitchingDialog(any()); - continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false); - verifySystemUserVisibilityChangesNeverNotified(); - } - - @Test - public void testContinueUserSwitchDismissKeyguard() { - when(mInjector.mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(false); - mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, - /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false, - /* backgroundUserScheduledStopTimeSecs= */ -1); - // Start user -- this will update state of mUserController - mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND); - Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); - assertNotNull(reportMsg); - UserState userState = (UserState) reportMsg.obj; - int oldUserId = reportMsg.arg1; - int newUserId = reportMsg.arg2; - mInjector.mHandler.clearAllRecordedMessages(); - // Verify that continueUserSwitch worked as expected - continueAndCompleteUserSwitch(userState, oldUserId, newUserId); - verify(mInjector, times(1)).dismissKeyguard(any()); verify(mInjector, times(1)).dismissUserSwitchingDialog(any()); continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false); verifySystemUserVisibilityChangesNeverNotified(); @@ -1923,11 +1900,6 @@ public class UserControllerTest { } @Override - protected void dismissKeyguard(Runnable runnable) { - runnable.run(); - } - - @Override void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser, String switchingFromSystemUserMessage, String switchingToSystemUserMessage, Runnable onShown) { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index 60a4b9a499c1..776f05dfb6ea 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -402,8 +402,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - /** - public void testPushDynamicShortcut() { + public void disabled_testPushDynamicShortcut() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5," + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1"); @@ -544,9 +543,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage( eq(CALLING_PACKAGE_1), eq("s9"), eq(USER_10)); } - */ - public void testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() + public void disabled_testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() throws InterruptedException { mService.updateConfigurationLocked( ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=500"); @@ -629,7 +627,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(3, mManager.getRemainingCallCount()); } - public void testPublishWithNoActivity() { + public void disbabledTestPublishWithNoActivity() { // If activity is not explicitly set, use the default one. mRunningUsers.put(USER_11, true); @@ -735,7 +733,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPublishWithNoActivity_noMainActivityInPackage() { + public void disabled_testPublishWithNoActivity_noMainActivityInPackage() { mRunningUsers.put(USER_11, true); runWithCaller(CALLING_PACKAGE_2, USER_11, () -> { @@ -2559,7 +2557,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPinShortcutAndGetPinnedShortcuts_multi() { + public void disabled_testPinShortcutAndGetPinnedShortcuts_multi() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -2891,7 +2889,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() { + public void disabled_testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -3921,7 +3919,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Try save and load, also stop/start the user. */ - public void testSaveAndLoadUser() { + public void disabled_testSaveAndLoadUser() { // First, create some shortcuts and save. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16); @@ -8581,7 +8579,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testReturnedByServer() { + public void disabled_testReturnedByServer() { // Package 1 updated, with manifest shortcuts. addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java index 2ed71cecd79d..952d8fa47a34 100644 --- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java @@ -118,7 +118,6 @@ public class ThermalManagerServiceTest { private class ThermalHalFake extends ThermalHalWrapper { private static final int INIT_STATUS = Temperature.THROTTLING_NONE; private List<Temperature> mTemperatureList = new ArrayList<>(); - private List<Temperature> mOverrideTemperatures = null; private List<CoolingDevice> mCoolingDeviceList = new ArrayList<>(); private List<TemperatureThreshold> mTemperatureThresholdList = initializeThresholds(); @@ -132,6 +131,9 @@ public class ThermalManagerServiceTest { INIT_STATUS); private CoolingDevice mCpu = new CoolingDevice(40, CoolingDevice.TYPE_BATTERY, "cpu"); private CoolingDevice mGpu = new CoolingDevice(43, CoolingDevice.TYPE_BATTERY, "gpu"); + private Map<Integer, Float> mForecastSkinTemperatures = null; + private int mForecastSkinTemperaturesCalled = 0; + private boolean mForecastSkinTemperaturesError = false; private List<TemperatureThreshold> initializeThresholds() { ArrayList<TemperatureThreshold> thresholds = new ArrayList<>(); @@ -173,12 +175,17 @@ public class ThermalManagerServiceTest { mCoolingDeviceList.add(mGpu); } - void setOverrideTemperatures(List<Temperature> temperatures) { - mOverrideTemperatures = temperatures; + void enableForecastSkinTemperature() { + mForecastSkinTemperatures = Map.of(0, 22.0f, 10, 25.0f, 20, 28.0f, + 30, 31.0f, 40, 34.0f, 50, 37.0f, 60, 40.0f); } - void resetOverrideTemperatures() { - mOverrideTemperatures = null; + void disableForecastSkinTemperature() { + mForecastSkinTemperatures = null; + } + + void failForecastSkinTemperature() { + mForecastSkinTemperaturesError = true; } @Override @@ -219,6 +226,18 @@ public class ThermalManagerServiceTest { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + mForecastSkinTemperaturesCalled++; + if (mForecastSkinTemperaturesError) { + throw new RuntimeException(); + } + if (mForecastSkinTemperatures == null) { + throw new UnsupportedOperationException(); + } + return mForecastSkinTemperatures.get(forecastSeconds); + } + + @Override protected boolean connectToHal() { return true; } @@ -388,7 +407,7 @@ public class ThermalManagerServiceTest { Thread.sleep(CALLBACK_TIMEOUT_MILLI_SEC); resetListenerMock(); int status = Temperature.THROTTLING_SEVERE; - mFakeHal.setOverrideTemperatures(new ArrayList<>()); + mFakeHal.mTemperatureList = new ArrayList<>(); // Should not notify on non-skin type Temperature newBattery = new Temperature(37, Temperature.TYPE_BATTERY, "batt", status); @@ -518,6 +537,99 @@ public class ThermalManagerServiceTest { } @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast() throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + assertEquals(1.0f, mService.mService.getThermalHeadroom(60), 0.01f); + assertEquals(0.9f, mService.mService.getThermalHeadroom(50), 0.01f); + assertEquals(0.8f, mService.mService.getThermalHeadroom(40), 0.01f); + assertEquals(0.7f, mService.mService.getThermalHeadroom(30), 0.01f); + assertEquals(0.6f, mService.mService.getThermalHeadroom(20), 0.01f); + assertEquals(0.5f, mService.mService.getThermalHeadroom(10), 0.01f); + assertEquals(0.4f, mService.mService.getThermalHeadroom(0), 0.01f); + assertEquals(7, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholds() + throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + List<TemperatureThreshold> thresholds = mFakeHal.initializeThresholds(); + TemperatureThreshold skinThreshold = new TemperatureThreshold(); + skinThreshold.type = Temperature.TYPE_SKIN; + skinThreshold.name = "skin2"; + skinThreshold.hotThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/]; + skinThreshold.coldThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/]; + for (int i = 0; i < skinThreshold.hotThrottlingThresholds.length; ++i) { + // Sets NONE to 45.0f, SEVERE to 60.0f, and SHUTDOWN to 75.0f + skinThreshold.hotThrottlingThresholds[i] = 45.0f + 5.0f * i; + } + thresholds.add(skinThreshold); + mFakeHal.mTemperatureThresholdList = thresholds; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertFalse("HAL skin forecast should be disabled on multiple SKIN thresholds", + mService.mIsHalSkinForecastSupported.get()); + mService.mService.getThermalHeadroom(10); + assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST, + Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK}) + public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholdsCallback() + throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + TemperatureThreshold newThreshold = new TemperatureThreshold(); + newThreshold.name = "skin2"; + newThreshold.type = Temperature.TYPE_SKIN; + newThreshold.hotThrottlingThresholds = new float[]{ + Float.NaN, 43.0f, 46.0f, 49.0f, Float.NaN, Float.NaN, Float.NaN + }; + mFakeHal.mCallback.onThresholdChanged(newThreshold); + mService.mService.getThermalHeadroom(10); + assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast_errorOnHal() throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + mFakeHal.disableForecastSkinTemperature(); + assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.enableForecastSkinTemperature(); + assertFalse(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(2, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.failForecastSkinTemperature(); + assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(3, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test @EnableFlags({Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK, Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS}) public void testTemperatureWatcherUpdateSevereThresholds() throws Exception { diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java new file mode 100644 index 000000000000..23c13bd04368 --- /dev/null +++ b/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java @@ -0,0 +1,527 @@ +/* + * Copyright 2024 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.timezonedetector; + +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.AUTO_REVERT_THRESHOLD; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.SIGNAL_TYPE_NONE; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.SIGNAL_TYPE_UNKNOWN; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_REJECTED; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_SUPERSEDED; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_UNKNOWN; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_UNTRACKED; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.spy; + +import android.annotation.Nullable; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.UiAutomation; +import android.content.Context; +import android.os.HandlerThread; +import android.os.Process; +import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.flags.Flags; +import com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.TimeZoneChangeRecord; +import com.android.server.timezonedetector.TimeZoneChangeListener.TimeZoneChangeEvent; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.time.InstantSource; +import java.util.ArrayList; +import java.util.List; + +/** + * White-box unit tests for {@link NotifyingTimeZoneChangeListener}. + */ +@RunWith(JUnitParamsRunner.class) +@EnableFlags(Flags.FLAG_DATETIME_NOTIFICATIONS) +public class NotifyingTimeZoneChangeListenerTest { + + @ClassRule + public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + + @Rule(order = 0) + public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + + @Rule(order = 1) + public final MockitoRule mockito = MockitoJUnit.rule(); + + public static List<@TimeZoneDetectorStrategy.Origin Integer> getDetectionOrigins() { + return List.of(ORIGIN_LOCATION, ORIGIN_TELEPHONY); + } + + private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234; + /** A time zone used for initialization that does not occur elsewhere in tests. */ + private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC"; + private static final String INTERACT_ACROSS_USERS_FULL_PERMISSION = + "android.permission.INTERACT_ACROSS_USERS_FULL"; + + @Mock + private Context mContext; + private UiAutomation mUiAutomation; + + private FakeNotificationManager mNotificationManager; + private HandlerThread mHandlerThread; + private TestHandler mHandler; + private FakeServiceConfigAccessor mServiceConfigAccessor; + private int mUid; + + private NotifyingTimeZoneChangeListener mTimeZoneChangeTracker; + + @Before + public void setUp() { + mUid = Process.myUid(); + // Create a thread + handler for processing the work that the service posts. + mHandlerThread = new HandlerThread("TimeZoneDetectorInternalTest"); + mHandlerThread.start(); + mHandler = new TestHandler(mHandlerThread.getLooper()); + + ConfigurationInternal config = new ConfigurationInternal.Builder() + .setUserId(mUid) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .setNotificationsSupported(true) + .setNotificationsTrackingSupported(true) + .setNotificationsEnabledSetting(false) + .setManualChangeTrackingSupported(false) + .build(); + + mServiceConfigAccessor = spy(new FakeServiceConfigAccessor()); + mServiceConfigAccessor.initializeCurrentUserConfiguration(config); + + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); + mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL_PERMISSION); + + mNotificationManager = new FakeNotificationManager(mContext, InstantSource.system()); + + mTimeZoneChangeTracker = new NotifyingTimeZoneChangeListener(mHandler, mContext, + mServiceConfigAccessor, mNotificationManager); + } + + @After + public void tearDown() throws Exception { + mHandlerThread.quit(); + mHandlerThread.join(); + } + + @Test + public void process_autoDetectionOff_noManualTracking_shouldTrackWithoutNotifying() { + enableTimeZoneNotifications(); + + TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + + assertEquals(expectedChangeEvent, mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + assertEquals(0, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(0); + } + + @Test + public void process_autoDetectionOff_shouldTrackWithoutNotifying() { + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + + assertEquals(expectedChangeEvent, mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + assertEquals(0, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + } + + @Test + @Parameters(method = "getDetectionOrigins") + public void process_automaticDetection_trackingSupported( + @TimeZoneDetectorStrategy.Origin int origin) { + if (origin == ORIGIN_LOCATION) { + enableLocationTimeZoneDetection(); + } else if (origin == ORIGIN_TELEPHONY) { + enableTelephonyTimeZoneDetection(); + } else { + throw new IllegalStateException( + "The given origin has not been implemented for this test: " + origin); + } + + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + // lastTrackedChangeEvent == null + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(expectedChangeEvent, trackedEvent1); + assertEquals(1, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + + expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 2, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 1000L, + /* unixEpochTimeMillis= */ 1726597800000L + 1000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/London", + /* newZoneId= */ "Europe/Paris", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + // lastTrackedChangeEvent != null + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(STATUS_SUPERSEDED, trackedEvent1.getStatus()); + assertEquals(expectedChangeEvent, trackedEvent2); + assertEquals(2, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(2); + + disableTimeZoneAutoDetection(); + + // Test manual change within revert threshold + { + expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 3, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 999L + AUTO_REVERT_THRESHOLD, + /* unixEpochTimeMillis= */ + 1726597800000L + 999L + AUTO_REVERT_THRESHOLD, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent3 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + // The user manually changed the time zone within a short period of receiving the + // notification, indicating that they rejected the automatic change of time zone + assertEquals(STATUS_REJECTED, trackedEvent2.getStatus()); + assertEquals(expectedChangeEvent, trackedEvent3); + assertEquals(2, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(3); + } + + // Test manual change outside of revert threshold + { + // [START] Reset previous event + enableNotificationsWithManualChangeTracking(); + mTimeZoneChangeTracker.process(trackedEvent2.getEvent()); + trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + disableTimeZoneAutoDetection(); + // [END] Reset previous event + + expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 5, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 1001L + AUTO_REVERT_THRESHOLD, + /* unixEpochTimeMillis= */ + 1726597800000L + 1001L + AUTO_REVERT_THRESHOLD, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent3 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + // The user manually changed the time zone outside of the period we consider as a revert + assertEquals(STATUS_SUPERSEDED, trackedEvent2.getStatus()); + assertEquals(expectedChangeEvent, trackedEvent3); + assertEquals(3, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(5); + } + } + + @Test + @Parameters(method = "getDetectionOrigins") + public void process_automaticDetection_trackingSupported_missingTransition( + @TimeZoneDetectorStrategy.Origin int origin) { + if (origin == ORIGIN_LOCATION) { + enableLocationTimeZoneDetection(); + } else if (origin == ORIGIN_TELEPHONY) { + enableTelephonyTimeZoneDetection(); + } else { + throw new IllegalStateException( + "The given origin has not been implemented for this test: " + origin); + } + + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + // lastTrackedChangeEvent == null + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(expectedChangeEvent, trackedEvent1); + assertEquals(1, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + + expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 3, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 1000L, + /* unixEpochTimeMillis= */ 1726597800000L + 1000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Athens", + /* newZoneId= */ "Europe/Paris", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + // lastTrackedChangeEvent != null + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(STATUS_SUPERSEDED, trackedEvent1.getStatus()); + assertEquals(expectedChangeEvent, trackedEvent2); + assertEquals(2, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(2); + } + + @Test + @Parameters(method = "getDetectionOrigins") + public void process_automaticDetection_trackingSupported_sameOffset( + @TimeZoneDetectorStrategy.Origin int origin) { + if (origin == ORIGIN_LOCATION) { + enableLocationTimeZoneDetection(); + } else if (origin == ORIGIN_TELEPHONY) { + enableTelephonyTimeZoneDetection(); + } else { + throw new IllegalStateException( + "The given origin has not been implemented for this test: " + origin); + } + + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedChangeEvent = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/Rome", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedChangeEvent.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + // lastTrackedChangeEvent == null + mTimeZoneChangeTracker.process(expectedChangeEvent.getEvent()); + TimeZoneChangeRecord trackedEvent1 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(expectedChangeEvent, trackedEvent1); + // No notification sent for the same UTC offset + assertEquals(0, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + } + + private void enableLocationTimeZoneDetection() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setAutoDetectionEnabledSetting(true) + .setGeoDetectionFeatureSupported(true) + .setGeoDetectionEnabledSetting(true) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void enableTelephonyTimeZoneDetection() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setAutoDetectionEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .setTelephonyDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(true) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void enableTimeZoneNotifications() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setNotificationsSupported(true) + .setNotificationsTrackingSupported(true) + .setNotificationsEnabledSetting(true) + .setManualChangeTrackingSupported(false) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void enableNotificationsWithManualChangeTracking() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setNotificationsSupported(true) + .setNotificationsTrackingSupported(true) + .setNotificationsEnabledSetting(true) + .setManualChangeTrackingSupported(true) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void disableTimeZoneAutoDetection() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setAutoDetectionEnabledSetting(false) + .setGeoDetectionEnabledSetting(false) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private ConfigurationInternal.Builder toBuilder(ConfigurationInternal config) { + return new ConfigurationInternal.Builder() + .setUserId(config.getUserId()) + .setTelephonyDetectionFeatureSupported(config.isTelephonyDetectionSupported()) + .setGeoDetectionFeatureSupported(config.isGeoDetectionSupported()) + .setTelephonyFallbackSupported(config.isTelephonyFallbackSupported()) + .setGeoDetectionRunInBackgroundEnabled( + config.getGeoDetectionRunInBackgroundEnabledSetting()) + .setEnhancedMetricsCollectionEnabled(config.isEnhancedMetricsCollectionEnabled()) + .setUserConfigAllowed(config.isUserConfigAllowed()) + .setAutoDetectionEnabledSetting(config.getAutoDetectionEnabledSetting()) + .setLocationEnabledSetting(config.getLocationEnabledSetting()) + .setGeoDetectionEnabledSetting(config.getGeoDetectionEnabledSetting()) + .setNotificationsTrackingSupported(config.isNotificationTrackingSupported()) + .setNotificationsEnabledSetting(config.getNotificationsEnabledBehavior()) + .setNotificationsSupported(config.areNotificationsSupported()) + .setManualChangeTrackingSupported(config.isManualChangeTrackingSupported()); + } + + private static class FakeNotificationManager extends NotificationManager { + + private final List<Notification> mNotifications = new ArrayList<>(); + + FakeNotificationManager(Context context, InstantSource clock) { + super(context, clock); + } + + @Override + public void notifyAsUser(@Nullable String tag, int id, Notification notification, + UserHandle user) { + mNotifications.add(notification); + } + + public List<Notification> getNotifications() { + return mNotifications; + } + } +} diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java index 47a9b2c47173..9a01fa2eb966 100644 --- a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java +++ b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java @@ -41,6 +41,9 @@ import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_ import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_ENABLED; import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_USER_RESTRICTED_AUTO_ENABLED; import static com.android.server.timezonedetector.ConfigInternalForTests.USER_ID; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGH; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGHEST; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_LOW; @@ -73,16 +76,21 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.service.timezone.TimeZoneProviderStatus; import android.util.IndentingPrintWriter; import com.android.server.SystemTimeZone.TimeZoneConfidence; +import com.android.server.flags.Flags; import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedTelephonyTimeZoneSuggestion; import junitparams.JUnitParamsRunner; import junitparams.Parameters; import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -98,8 +106,14 @@ import java.util.function.Function; * White-box unit tests for {@link TimeZoneDetectorStrategyImpl}. */ @RunWith(JUnitParamsRunner.class) +@EnableFlags(Flags.FLAG_DATETIME_NOTIFICATIONS) public class TimeZoneDetectorStrategyImplTest { + @ClassRule + public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234; /** A time zone used for initialization that does not occur elsewhere in tests. */ private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC"; @@ -130,6 +144,7 @@ public class TimeZoneDetectorStrategyImplTest { private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy; private FakeEnvironment mFakeEnvironment; + private FakeTimeZoneChangeEventListener mFakeTimeZoneChangeEventTracker; private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy; @@ -139,9 +154,10 @@ public class TimeZoneDetectorStrategyImplTest { mFakeServiceConfigAccessorSpy = spy(new FakeServiceConfigAccessor()); mFakeServiceConfigAccessorSpy.initializeCurrentUserConfiguration( CONFIG_AUTO_DISABLED_GEO_DISABLED); + mFakeTimeZoneChangeEventTracker = new FakeTimeZoneChangeEventListener(); mTimeZoneDetectorStrategy = new TimeZoneDetectorStrategyImpl( - mFakeServiceConfigAccessorSpy, mFakeEnvironment); + mFakeServiceConfigAccessorSpy, mFakeEnvironment, mFakeTimeZoneChangeEventTracker); } @Test @@ -363,6 +379,10 @@ public class TimeZoneDetectorStrategyImplTest { // SlotIndex1 should always beat slotIndex2, all other things being equal. assertEquals(expectedSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } /** @@ -398,6 +418,10 @@ public class TimeZoneDetectorStrategyImplTest { SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } // A good quality suggestion will be used. @@ -415,6 +439,13 @@ public class TimeZoneDetectorStrategyImplTest { SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + assertEquals(ORIGIN_TELEPHONY, + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().getFirst() + .getOrigin()); + } } // A low quality suggestion will be accepted, but not used to set the device time zone. @@ -432,6 +463,11 @@ public class TimeZoneDetectorStrategyImplTest { SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + // Still 1 from last good quality suggestion but not recorded as quality is too low + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } } @@ -492,6 +528,17 @@ public class TimeZoneDetectorStrategyImplTest { assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); } + + if (Flags.datetimeNotifications()) { + /* + * Only 6 out of 7 tests have a quality good enough to trigger an event and the + * configuration is reset at every loop. + */ + assertEquals(6, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + assertEquals(ORIGIN_TELEPHONY, + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().getFirst() + .getOrigin()); + } } @Test @@ -518,6 +565,18 @@ public class TimeZoneDetectorStrategyImplTest { for (TelephonyTestCase testCase : descendingCasesByScore) { makeSlotIndex1SuggestionAndCheckState(script, testCase); } + + if (Flags.datetimeNotifications()) { + /* + * Only 6 out of 7 tests have a quality good enough to trigger an event and the + * set of tests is run twice. + */ + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(12, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.getFirst().getOrigin()); + } } private void makeSlotIndex1SuggestionAndCheckState(Script script, TelephonyTestCase testCase) { @@ -641,6 +700,18 @@ public class TimeZoneDetectorStrategyImplTest { .verifyLatestQualifiedTelephonySuggestionReceived( SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion); } + + if (Flags.datetimeNotifications()) { + /* + * Only 6 out of 7 tests have a quality good enough to trigger an event and the + * simulation runs twice per loop with a different time zone (i.e. London and Paris). + */ + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(12, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.getFirst().getOrigin()); + } } /** @@ -683,6 +754,14 @@ public class TimeZoneDetectorStrategyImplTest { // Latest suggestion should be used. script.simulateSetAutoMode(true) .verifyTimeZoneChangedAndReset(newYorkSuggestion); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(2, timeZoneChangeEvents.size()); + assertTrue(timeZoneChangeEvents.stream() + .allMatch(x -> x.getOrigin() == ORIGIN_TELEPHONY)); + } } @Test @@ -714,6 +793,10 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged(); assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -732,6 +815,15 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneChangedAndReset(manualSuggestion); assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion()); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin() + ); + } } @Test @@ -754,6 +846,10 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged(); assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -780,6 +876,16 @@ public class TimeZoneDetectorStrategyImplTest { script.verifyTimeZoneNotChanged(); assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion()); } + + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + if (Flags.datetimeNotifications() && expectedResult) { + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin()); + } else { + assertEmpty(timeZoneChangeEvents); + } } @Test @@ -830,6 +936,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } { @@ -857,6 +967,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } } @@ -893,6 +1007,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -927,6 +1045,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -962,6 +1084,14 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.getFirst().getOrigin()); + } } /** @@ -999,6 +1129,17 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(londonOrParisEvent) .verifyTimeZoneNotChanged() .verifyLatestLocationAlgorithmEventReceived(londonOrParisEvent); + + if (Flags.datetimeNotifications()) { + // we do not record events if the time zone does not change (i.e. 2 / 4 of the + // simulated cases) + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(2, timeZoneChangeEvents.size()); + assertTrue(timeZoneChangeEvents.stream() + .allMatch(x -> x.getOrigin() == ORIGIN_LOCATION)); + } } /** @@ -1059,6 +1200,16 @@ public class TimeZoneDetectorStrategyImplTest { // A configuration change is considered a "state change". assertStateChangeNotificationsSent(stateChangeListener, 1); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(3, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(2).getOrigin()); + } } @Test @@ -1088,6 +1239,14 @@ public class TimeZoneDetectorStrategyImplTest { .simulateTelephonyTimeZoneSuggestion(telephonySuggestion) .verifyTimeZoneChangedAndReset(telephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin()); + } } // Receiving an "uncertain" geolocation suggestion should have no effect. @@ -1098,6 +1257,11 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Receiving a "certain" geolocation suggestion should disable telephony fallback mode. @@ -1109,6 +1273,14 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(2, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin()); + } } // Used to record the last telephony suggestion received, which will be used when fallback @@ -1125,6 +1297,11 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); lastTelephonySuggestion = telephonySuggestion; + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(2, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Geolocation suggestions should continue to be used as normal (previous telephony @@ -1151,6 +1328,14 @@ public class TimeZoneDetectorStrategyImplTest { // No change needed, device will already be set to Europe/Rome. .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(3, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(2).getOrigin()); + } } // Enable telephony fallback. Nothing will change, because the geolocation is still certain, @@ -1160,6 +1345,11 @@ public class TimeZoneDetectorStrategyImplTest { .simulateEnableTelephonyFallback() .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(3, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Make the geolocation algorithm uncertain. @@ -1170,6 +1360,14 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(lastTelephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(4, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(3).getOrigin()); + } } // Make the geolocation algorithm certain, disabling telephony fallback. @@ -1181,6 +1379,14 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(5, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(4).getOrigin()); + } } // Demonstrate what happens when geolocation is uncertain when telephony fallback is @@ -1195,6 +1401,14 @@ public class TimeZoneDetectorStrategyImplTest { .simulateEnableTelephonyFallback() .verifyTimeZoneChangedAndReset(lastTelephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(6, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(5).getOrigin()); + } } } @@ -1225,6 +1439,13 @@ public class TimeZoneDetectorStrategyImplTest { .simulateTelephonyTimeZoneSuggestion(telephonySuggestion) .verifyTimeZoneChangedAndReset(telephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin()); + } } // Receiving an "uncertain" geolocation suggestion without a status should have no effect. @@ -1235,6 +1456,11 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Receiving a "certain" geolocation suggestion should disable telephony fallback mode. @@ -1246,6 +1472,13 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(2, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin()); + } } // Used to record the last telephony suggestion received, which will be used when fallback @@ -1262,6 +1495,11 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); lastTelephonySuggestion = telephonySuggestion; + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(2, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Geolocation suggestions should continue to be used as normal (previous telephony @@ -1291,6 +1529,13 @@ public class TimeZoneDetectorStrategyImplTest { // No change needed, device will already be set to Europe/Rome. .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(3, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(2).getOrigin()); + } } // Enable telephony fallback via a LocationAlgorithmEvent containing an "uncertain" @@ -1310,6 +1555,13 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(uncertainEventBlockedBySettings) .verifyTimeZoneChangedAndReset(lastTelephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(4, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(3).getOrigin()); + } } // Make the geolocation algorithm certain, disabling telephony fallback. @@ -1321,6 +1573,13 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(5, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(4).getOrigin()); + } } } @@ -1349,6 +1608,10 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } // Make an uncertain geolocation suggestion, there is no telephony suggestion to fall back @@ -1360,6 +1623,10 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } // Similar to the case above, but force a fallback attempt after making a "certain" @@ -1386,6 +1653,13 @@ public class TimeZoneDetectorStrategyImplTest { .simulateEnableTelephonyFallback() .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(0).getOrigin()); + } } } @@ -1803,6 +2077,16 @@ public class TimeZoneDetectorStrategyImplTest { userId, manualTimeZoneSuggestion, bypassUserPolicyChecks); assertEquals(expectedResult, actualResult); + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + if (actualResult && Flags.datetimeNotifications()) { + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin()); + } else { + assertEmpty(timeZoneChangeEvents); + } + return this; } diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index 23dcb65eb30f..dc16de1aab5e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -43,6 +43,7 @@ import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -92,6 +93,7 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; /** * Tests for the {@link DragDropController} class. @@ -255,7 +257,7 @@ public class DragDropControllerTests extends WindowTestsBase { iwindow.setDragEventJournal(dragEvents); startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { // Verify the start-drag event is sent for invisible windows final DragEvent dragEvent = dragEvents.get(0); assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED); @@ -297,7 +299,7 @@ public class DragDropControllerTests extends WindowTestsBase { globalInterceptIWindow.setDragEventJournal(globalInterceptWindowDragEvents); startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, - createClipDataForActivity(null, mock(UserHandle.class)), () -> { + createClipDataForActivity(null, mock(UserHandle.class)), (unused) -> { // Verify the start-drag event is sent for the local and global intercept window // but not the other window assertTrue(nonLocalWindowDragEvents.isEmpty()); @@ -340,7 +342,7 @@ public class DragDropControllerTests extends WindowTestsBase { iwindow.setDragEventJournal(dragEvents); startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { // Verify the start-drag event has the drag flags final DragEvent dragEvent = dragEvents.get(0); assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED); @@ -386,7 +388,7 @@ public class DragDropControllerTests extends WindowTestsBase { iwindow2.setDragEventJournal(dragEvents2); startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { // Verify the start-drag event is sent as-is for the drag origin window. final DragEvent dragEvent = dragEvents.get(0); assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction()); @@ -413,8 +415,14 @@ public class DragDropControllerTests extends WindowTestsBase { assertEquals(ACTION_DROP, dropEvent.getAction()); assertEquals(dropCoordsPx, dropEvent.getX(), 0.0 /* delta */); assertEquals(dropCoordsPx, dropEvent.getY(), 0.0 /* delta */); + assertEquals(window2.getDisplayId(), dropEvent.getDisplayId()); mTarget.reportDropResult(iwindow2, true); + // Verify both windows received ACTION_DRAG_ENDED event. + assertEquals(ACTION_DRAG_ENDED, last(dragEvents).getAction()); + assertEquals(window2.getDisplayId(), last(dragEvents).getDisplayId()); + assertEquals(ACTION_DRAG_ENDED, last(dragEvents2).getAction()); + assertEquals(window2.getDisplayId(), last(dragEvents2).getDisplayId()); } finally { mTarget.mDeferDragStateClosed = false; } @@ -441,7 +449,7 @@ public class DragDropControllerTests extends WindowTestsBase { iwindow2.setDragEventJournal(dragEvents2); startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { // Verify the start-drag event is sent as-is for the drag origin window. final DragEvent dragEvent = dragEvents.get(0); assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction()); @@ -456,10 +464,12 @@ public class DragDropControllerTests extends WindowTestsBase { try { mTarget.mDeferDragStateClosed = true; + mTarget.handleMotionEvent(true, testDisplay.getDisplayId(), dropCoordsPx, + dropCoordsPx); // x, y is window-local coordinate. mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx, dropCoordsPx); - mTarget.handleMotionEvent(false, window2.getDisplayId(), dropCoordsPx, + mTarget.handleMotionEvent(false, testDisplay.getDisplayId(), dropCoordsPx, dropCoordsPx); mToken = window2.mClient.asBinder(); // Verify only window2 received the DROP event and coords are sent as-is @@ -469,14 +479,70 @@ public class DragDropControllerTests extends WindowTestsBase { assertEquals(ACTION_DROP, dropEvent.getAction()); assertEquals(dropCoordsPx, dropEvent.getX(), 0.0 /* delta */); assertEquals(dropCoordsPx, dropEvent.getY(), 0.0 /* delta */); + assertEquals(testDisplay.getDisplayId(), dropEvent.getDisplayId()); mTarget.reportDropResult(iwindow2, true); + // Verify both windows received ACTION_DRAG_ENDED event. + assertEquals(ACTION_DRAG_ENDED, last(dragEvents).getAction()); + assertEquals(testDisplay.getDisplayId(), last(dragEvents).getDisplayId()); + assertEquals(ACTION_DRAG_ENDED, last(dragEvents2).getAction()); + assertEquals(testDisplay.getDisplayId(), last(dragEvents2).getDisplayId()); } finally { mTarget.mDeferDragStateClosed = false; } }); } + @Test + public void testDragMove() { + startDrag(0, 0, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, + ClipData.newPlainText("label", "text"), (surface) -> { + int dragMoveX = mWindow.getBounds().centerX(); + int dragMoveY = mWindow.getBounds().centerY(); + final SurfaceControl.Transaction transaction = + mSystemServicesTestRule.mTransaction; + clearInvocations(transaction); + + mTarget.handleMotionEvent(true, mWindow.getDisplayId(), dragMoveX, dragMoveY); + verify(transaction).setPosition(surface, dragMoveX, dragMoveY); + + // Clean-up. + mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), 0, + 0); + mToken = mWindow.mClient.asBinder(); + }); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND) + public void testConnectedDisplaysDragMoveToOtherDisplay() { + final float testDensityMultiplier = 1.5f; + final DisplayContent testDisplay = createMockSimulatedDisplay(); + testDisplay.mBaseDisplayDensity = + (int) (mDisplayContent.mBaseDisplayDensity * testDensityMultiplier); + WindowState testWindow = createDropTargetWindow("App drag test window", testDisplay); + + // Test starts from mWindow which is on default display. + startDrag(0, 0, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, + ClipData.newPlainText("label", "text"), (surface) -> { + final SurfaceControl.Transaction transaction = + mSystemServicesTestRule.mTransaction; + clearInvocations(transaction); + mTarget.handleMotionEvent(true, testWindow.getDisplayId(), 0, 0); + + verify(transaction).reparent(surface, testDisplay.getSurfaceControl()); + verify(transaction).setScale(surface, testDensityMultiplier, + testDensityMultiplier); + + // Clean-up. + mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), 0, + 0); + mToken = mWindow.mClient.asBinder(); + }); + } + private DragEvent last(ArrayList<DragEvent> list) { return list.get(list.size() - 1); } @@ -645,7 +711,7 @@ public class DragDropControllerTests extends WindowTestsBase { startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED); // Verify after consuming that the drag surface is relinquished @@ -676,7 +742,7 @@ public class DragDropControllerTests extends WindowTestsBase { startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED); // Verify after consuming that the drag surface is relinquished @@ -713,7 +779,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "Test"), () -> { + ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), @@ -738,7 +804,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "Test"), () -> { + ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), @@ -761,7 +827,7 @@ public class DragDropControllerTests extends WindowTestsBase { doReturn(mock(Binder.class)).when(listener).asBinder(); mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; - startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> { + startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was not called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, mDisplayContent.getDisplayId(), @@ -784,7 +850,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "Test"), () -> { + ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, @@ -805,7 +871,7 @@ public class DragDropControllerTests extends WindowTestsBase { } private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) { - startDrag(flags, data, () -> { + startDrag(flags, data, (unused) -> { mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY); mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), dropX, dropY); @@ -816,27 +882,26 @@ public class DragDropControllerTests extends WindowTestsBase { /** * Starts a drag with the given parameters, calls Runnable `r` after drag is started. */ - private void startDrag(int flag, ClipData data, Runnable r) { - startDrag(0, 0, flag, data, r); + private void startDrag(int flag, ClipData data, Consumer<SurfaceControl> c) { + startDrag(0, 0, flag, data, c); } /** * Starts a drag with the given parameters, calls Runnable `r` after drag is started. */ private void startDrag(float startInWindowX, float startInWindowY, int flag, ClipData data, - Runnable r) { + Consumer<SurfaceControl> c) { final SurfaceSession appSession = new SurfaceSession(); try { final SurfaceControl surface = new SurfaceControl.Builder(appSession).setName( "drag surface").setBufferSize(100, 100).setFormat( PixelFormat.TRANSLUCENT).build(); - assertTrue(mWm.mInputManager.startDragAndDrop(new Binder(), new Binder())); mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, startInWindowX, startInWindowY, 0, 0, data); assertNotNull(mToken); - r.run(); + c.accept(surface); } finally { appSession.kill(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 96b11a87d8df..07ee09a350d9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -185,31 +185,46 @@ public class SizeCompatTests extends WindowTestsBase { } private ActivityRecord setUpApp(DisplayContent display) { - return setUpApp(display, null /* appBuilder */); + return setUpApp(display, null /* appBuilder */, null /* taskBuilder */); } private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder appBuilder) { + return setUpApp(display, appBuilder, null /* taskBuilder */); + } + + private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder aBuilder, + TaskBuilder tBuilder) { // Use the real package name (com.android.frameworks.wmtests) so that // EnableCompatChanges/DisableCompatChanges can take effect. // Otherwise the fake WindowTestsBase.DEFAULT_COMPONENT_PACKAGE_NAME will make // PlatformCompat#isChangeEnabledByPackageName always return default value. final ComponentName componentName = ComponentName.createRelative( mContext, SizeCompatTests.class.getName()); - mTask = new TaskBuilder(mSupervisor).setDisplay(display).setComponent(componentName) + final TaskBuilder taskBuilder = tBuilder != null ? tBuilder : new TaskBuilder(mSupervisor); + mTask = taskBuilder.setDisplay(display).setComponent(componentName) .build(); - final ActivityBuilder builder = appBuilder != null ? appBuilder : new ActivityBuilder(mAtm); - mActivity = builder.setTask(mTask).setComponent(componentName).build(); + final ActivityBuilder appBuilder = aBuilder != null ? aBuilder : new ActivityBuilder(mAtm); + mActivity = appBuilder.setTask(mTask).setComponent(componentName).build(); doReturn(false).when(mActivity).isImmersiveMode(any()); return mActivity; } private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh) { - return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */); + return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, null /* taskBuilder */); } private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder) { + return setUpDisplaySizeWithApp(dw, dh, appBuilder, null /* taskBuilder */); + } + + private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, TaskBuilder taskBuilder) { + return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, taskBuilder); + } + + private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder, + TaskBuilder taskBuilder) { final TestDisplayContent.Builder builder = new TestDisplayContent.Builder(mAtm, dw, dh); - return setUpApp(builder.build(), appBuilder); + return setUpApp(builder.build(), appBuilder, taskBuilder); } private void setUpLargeScreenDisplayWithApp(int dw, int dh) { @@ -4469,6 +4484,80 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(0, 0, 1000, 2800), bounds); } + @Test + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testUserAspectRatioOverridesNotAppliedToResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + spyOn(mActivity.mWmService.mAppCompatConfiguration); + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) + .isUserAppAspectRatioSettingsEnabled(); + final AppCompatController appCompatController = mActivity.mAppCompatController; + final AppCompatAspectRatioOverrides aspectRatioOverrides = + appCompatController.getAppCompatAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + // Set user aspect ratio override. + doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides) + .getUserMinAspectRatioOverrideCode(); + + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false); + assertFalse(appCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied()); + } + + @Test + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testUserAspectRatioOverridesAppliedToNonResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + spyOn(mActivity.mWmService.mAppCompatConfiguration); + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) + .isUserAppAspectRatioSettingsEnabled(); + final AppCompatController appCompatController = mActivity.mAppCompatController; + final AppCompatAspectRatioOverrides aspectRatioOverrides = + appCompatController.getAppCompatAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + // Set user aspect ratio override. + doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides) + .getUserMinAspectRatioOverrideCode(); + + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true); + assertTrue(appCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied()); + } + + @Test + @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO, + ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE}) + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testSystemAspectRatioOverridesNotAppliedToResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false); + + assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isAspectRatioApplied()); + } + + @Test + @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO, + ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE}) + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testSystemAspectRatioOverridesAppliedToNonResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true); + + assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + .isAspectRatioApplied()); + } + private void assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity( float letterboxVerticalPositionMultiplier, Rect fixedOrientationLetterbox, Rect sizeCompatUnscaled, Rect sizeCompatScaled) { diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index 5ed2df30518b..cc447a18758c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -1269,6 +1269,7 @@ public class WindowContainerTests extends WindowTestsBase { final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); spyOn(container); spyOn(surfaceAnimator); + doReturn(t).when(container).getSyncTransaction(); // Trigger for first relative layer call. container.assignRelativeLayer(t, relativeParent, 1 /* layer */); @@ -1295,6 +1296,7 @@ public class WindowContainerTests extends WindowTestsBase { spyOn(container); spyOn(surfaceAnimator); spyOn(surfaceFreezer); + doReturn(t).when(container).getSyncTransaction(); container.setLayer(t, 1); container.setRelativeLayer(t, relativeParent, 2); diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml index 914adc40194d..8d380f0d72a6 100644 --- a/tests/Input/AndroidManifest.xml +++ b/tests/Input/AndroidManifest.xml @@ -32,7 +32,7 @@ android:process=":externalProcess"> </activity> - <activity android:name="com.android.test.input.CaptureEventActivity" + <activity android:name="com.android.cts.input.CaptureEventActivity" android:label="Capture events" android:configChanges="touchscreen|uiMode|orientation|screenSize|screenLayout|keyboardHidden|uiMode|navigation|keyboard|density|fontScale|layoutDirection|locale|mcc|mnc|smallestScreenSize" android:enableOnBackInvokedCallback="false" diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index d35c9008e8cb..8c04f647fb2f 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -59,6 +59,7 @@ import junitparams.Parameters import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -1466,20 +1467,32 @@ class KeyGestureControllerTests { @Parameters(method = "customInputGesturesTestArguments") fun testCustomKeyGestures(test: TestData) { setupKeyGestureController() + val trigger = InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState + ) val builder = InputGestureData.Builder() .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger( - InputGestureData.createKeyTrigger( - test.expectedKeys[0], - test.expectedModifierState - ) - ) + .setTrigger(trigger) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } val inputGestureData = builder.build() - keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData) + assertNull( + test.toString(), + keyGestureController.getInputGesture(0, trigger.aidlTrigger) + ) + assertEquals( + test.toString(), + InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, + keyGestureController.addCustomInputGesture(0, builder.build().aidlData) + ) + assertEquals( + test.toString(), + inputGestureData.aidlData, + keyGestureController.getInputGesture(0, trigger.aidlTrigger) + ) testKeyGestureInternal(test) } diff --git a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt b/tests/Input/src/com/android/test/input/CaptureEventActivity.kt deleted file mode 100644 index d54e3470d9c4..000000000000 --- a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2024 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.test.input - -import android.app.Activity -import android.os.Bundle -import android.view.InputEvent -import android.view.KeyEvent -import android.view.MotionEvent -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit -import org.junit.Assert.assertNull - -class CaptureEventActivity : Activity() { - private val events = LinkedBlockingQueue<InputEvent>() - var shouldHandleKeyEvents = true - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Set the fixed orientation if requested - if (intent.hasExtra(EXTRA_FIXED_ORIENTATION)) { - val orientation = intent.getIntExtra(EXTRA_FIXED_ORIENTATION, 0) - setRequestedOrientation(orientation) - } - - // Set the flag if requested - if (intent.hasExtra(EXTRA_WINDOW_FLAGS)) { - val flags = intent.getIntExtra(EXTRA_WINDOW_FLAGS, 0) - window.addFlags(flags) - } - } - - override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - events.add(KeyEvent(event)) - return shouldHandleKeyEvents - } - - override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - fun getInputEvent(): InputEvent? { - return events.poll(5, TimeUnit.SECONDS) - } - - fun hasReceivedEvents(): Boolean { - return !events.isEmpty() - } - - fun assertNoEvents() { - val event = events.poll(100, TimeUnit.MILLISECONDS) - assertNull("Expected no events, but received $event", event) - } - - companion object { - const val EXTRA_FIXED_ORIENTATION = "fixed_orientation" - const val EXTRA_WINDOW_FLAGS = "window_flags" - } -} diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt index 0b281d8d39e2..9e0f7347943d 100644 --- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt +++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt @@ -28,6 +28,7 @@ import android.view.InputEvent import android.view.MotionEvent import androidx.test.platform.app.InstrumentationRegistry import com.android.cts.input.BatchedEventSplitter +import com.android.cts.input.CaptureEventActivity import com.android.cts.input.InputJsonParser import com.android.cts.input.VirtualDisplayActivityScenario import com.android.cts.input.inputeventmatchers.isResampled diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index 20315561cceb..f00a6cad6b46 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -54,7 +54,9 @@ std::string GetSafePath(StringPiece arg) { void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + if (value) { + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + } return true; }; @@ -65,7 +67,9 @@ void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::st void Command::AddRequiredFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + if (value) { + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + } return true; }; @@ -76,7 +80,9 @@ void Command::AddRequiredFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlag(StringPiece name, StringPiece description, std::optional<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + if (value) { + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + } return true; }; @@ -87,7 +93,9 @@ void Command::AddOptionalFlag(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + if (value) { + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + } return true; }; @@ -98,7 +106,9 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::unordered_set<std::string>* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - value->emplace(arg); + if (value) { + value->emplace(arg); + } return true; }; @@ -108,7 +118,9 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - *value = true; + if (value) { + *value = true; + } return true; }; diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp index 2a3cb2a0c65d..ad167c979662 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -159,4 +159,22 @@ TEST(CommandTest, ShortOptions) { ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr)); } +TEST(CommandTest, OptionsWithNullptrToAcceptValues) { + TestCommand command; + command.AddRequiredFlag("--rflag", "", nullptr); + command.AddRequiredFlagList("--rlflag", "", nullptr); + command.AddOptionalFlag("--oflag", "", nullptr); + command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr); + command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr); + command.AddOptionalSwitch("--switch", "", nullptr); + + ASSERT_EQ(0, command.Execute({ + "--rflag"s, "1"s, + "--rlflag"s, "1"s, + "--oflag"s, "1"s, + "--olflag"s, "1"s, + "--olflag2"s, "1"s, + "--switch"s}, &std::cerr)); +} + } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index 6c3eae11eab9..060bc5fa2242 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -425,9 +425,6 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { << output_format_.value()); return 1; } - if (enable_sparse_encoding_) { - table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled; - } if (force_sparse_encoding_) { table_flattener_options_.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 9452e588953e..98c8f5ff89c0 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -36,11 +36,9 @@ class ConvertCommand : public Command { kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_); AddOptionalSwitch( "--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", - &enable_sparse_encoding_); + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" @@ -87,7 +85,6 @@ class ConvertCommand : public Command { std::string output_path_; std::optional<std::string> output_format_; bool verbose_ = false; - bool enable_sparse_encoding_ = false; bool force_sparse_encoding_ = false; bool enable_compact_entries_ = false; std::optional<std::string> resources_config_path_; diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 232b4024abd2..eb71189ffc46 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -2504,9 +2504,6 @@ int LinkCommand::Action(const std::vector<std::string>& args) { << "the --merge-only flag can be only used when building a static library"); return 1; } - if (options_.use_sparse_encoding) { - options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; - } // The default build type. context.SetPackageType(PackageType::kApp); diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 2f17853718ec..b5bd905c02be 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -75,7 +75,6 @@ struct LinkOptions { bool no_resource_removal = false; bool no_xml_namespaces = false; bool do_not_compress_anything = false; - bool use_sparse_encoding = false; std::unordered_set<std::string> extensions_to_not_compress; std::optional<std::regex> regex_to_not_compress; FeatureFlagValues feature_flag_values; @@ -163,9 +162,11 @@ class LinkCommand : public Command { AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n" "defaults. Use this only when building runtime resource overlay packages.", &options_.no_resource_removal); - AddOptionalSwitch("--enable-sparse-encoding", - "This decreases APK size at the cost of resource retrieval performance.", - &options_.use_sparse_encoding); + AddOptionalSwitch( + "--enable-sparse-encoding", + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", &options_.table_flattener_options.use_compact_entries); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 762441ee1872..f218307af578 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -406,9 +406,6 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) { return 1; } - if (options_.enable_sparse_encoding) { - options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; - } if (options_.force_sparse_encoding) { options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 012b0f230ca2..e3af584cbbd9 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -61,9 +61,6 @@ struct OptimizeOptions { // TODO(b/246489170): keep the old option and format until transform to the new one std::optional<std::string> shortened_paths_map_path; - // Whether sparse encoding should be used for O+ resources. - bool enable_sparse_encoding = false; - // Whether sparse encoding should be used for all resources. bool force_sparse_encoding = false; @@ -106,11 +103,9 @@ class OptimizeCommand : public Command { &kept_artifacts_); AddOptionalSwitch( "--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", - &options_.enable_sparse_encoding); + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 1a82021bce71..b8ac7925d44e 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -201,7 +201,7 @@ class PackageFlattener { (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) { // Sparse encode if forced or sdk version is not set in context and config. } else { - // Otherwise, only sparse encode if the entries will be read on platforms S_V2+. + // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32). sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2); } diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index 0633bc81cb25..f1c4c3512ed3 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -37,8 +37,7 @@ constexpr const size_t kSparseEncodingThreshold = 60; enum class SparseEntriesMode { // Disables sparse encoding for entries. Disabled, - // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less - // than O only applies sparse encoding for resource configuration available on O+. + // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2). Enabled, // Enables sparse encoding for all entries regardless of minSdk. Forced, @@ -47,7 +46,7 @@ enum class SparseEntriesMode { struct TableFlattenerOptions { // When enabled, types for configurations with a sparse set of entries are encoded // as a sparse map of entry ID and offset to actual data. - SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled; + SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled; // When true, use compact entries for simple data bool use_compact_entries = false; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index 0f1168514c4a..e3d589eb078b 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -337,13 +337,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Enabled; + options.sparse_entries = SparseEntriesMode::Disabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); @@ -421,13 +421,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Enabled; + options.sparse_entries = SparseEntriesMode::Disabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); diff --git a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt index 6da6fc6f12c3..d0807f2ecd34 100644 --- a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt +++ b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt @@ -877,7 +877,7 @@ resource_table { } tool_fingerprint { tool: "Android Asset Packaging Tool (aapt)" - version: "2.19-SOONG BUILD NUMBER PLACEHOLDER" + version: "2.20-SOONG BUILD NUMBER PLACEHOLDER" } } xml_files { diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 8368f9d16af8..664d8412a3be 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -1,5 +1,11 @@ # Android Asset Packaging Tool 2.0 (AAPT2) release notes +## Version 2.20 +- Too many features, bug fixes, and improvements to list since the last minor version update in + 2017. This README will be updated more frequently in the future. +- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The + `--enable-sparse-encoding` flag still exists, but is a no-op. + ## Version 2.19 - Added navigation resource type. - Fixed issue with resource deduplication. (bug 64397629) diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp index 3d83caf29bba..6a4dfa629394 100644 --- a/tools/aapt2/util/Util.cpp +++ b/tools/aapt2/util/Util.cpp @@ -227,7 +227,7 @@ std::string GetToolFingerprint() { static const char* const sMajorVersion = "2"; // Update minor version whenever a feature or flag is added. - static const char* const sMinorVersion = "19"; + static const char* const sMinorVersion = "20"; // The build id of aapt2 binary. static const std::string sBuildId = [] { diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt index ea660b013893..22d364ec3212 100644 --- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -263,7 +263,7 @@ object SystemFeaturesGenerator { .returns(Boolean::class.java) .addParameter(CONTEXT_CLASS, "context") .addParameter(String::class.java, "featureName") - .addStatement("return context.getPackageManager().hasSystemFeature(featureName, 0)") + .addStatement("return context.getPackageManager().hasSystemFeature(featureName)") .build() ) } diff --git a/tools/systemfeatures/tests/golden/RoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoFeatures.java.gen index ee97b26159de..730dacbbf995 100644 --- a/tools/systemfeatures/tests/golden/RoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoFeatures.java.gen @@ -70,7 +70,7 @@ public final class RoFeatures { } private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen index 40c7db7ff1df..fe268c70708e 100644 --- a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen @@ -25,7 +25,7 @@ public final class RoNoFeatures { } private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/golden/RwFeatures.java.gen b/tools/systemfeatures/tests/golden/RwFeatures.java.gen index 7bf89614b92d..bcf978de3c1f 100644 --- a/tools/systemfeatures/tests/golden/RwFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwFeatures.java.gen @@ -55,7 +55,7 @@ public final class RwFeatures { } private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen index eb7ec63f1d7d..7bad5a2bae2a 100644 --- a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen @@ -14,7 +14,7 @@ import android.util.ArrayMap; */ public final class RwNoFeatures { private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java index ed3f5c94ba79..491b55e7992c 100644 --- a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java +++ b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java @@ -76,28 +76,28 @@ public class SystemFeaturesGeneratorTest { // Also ensure we fall back to the PackageManager for feature APIs without an accompanying // versioned feature definition. - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false); assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse(); } @Test public void testReadonlyDisabledWithDefinedFeatures() { // Always fall back to the PackageManager for defined, explicit features queries. - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false); assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true); assertThat(RwFeatures.hasFeatureWifi(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN)).thenReturn(false); assertThat(RwFeatures.hasFeatureVulkan(mContext)).isFalse(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(false); assertThat(RwFeatures.hasFeatureAuto(mContext)).isFalse(); // For defined and undefined features, conditional queries should report null (unknown). @@ -139,9 +139,9 @@ public class SystemFeaturesGeneratorTest { assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse(); // VERSION= - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(false); assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(true); assertThat(RoFeatures.hasFeatureAuto(mContext)).isTrue(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isNull(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); @@ -149,9 +149,9 @@ public class SystemFeaturesGeneratorTest { // For feature APIs without an associated feature definition, conditional queries should // report null, and explicit queries should report runtime-defined versions. - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(true); assertThat(RoFeatures.hasFeaturePc(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(false); assertThat(RoFeatures.hasFeaturePc(mContext)).isFalse(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, -1)).isNull(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, 0)).isNull(); |